代码拉取完成,页面将自动刷新
线程的六种状态:
NEW
BLOCKED
RUNNABLE
WAITING
TIMED_WAITING
TERMINATED
创建线程的多种方式:
1.继承Thread类
2.实现Runnable接口
3.匿名内部类
4.带返回值的线程
5.定时器
6.线程池的实现
7.Lambda表达式实现
8.Unsafe
多线程问题:
线程安全性问题
活跃性问题
死锁
争夺同一个资源
饥饿
高优先级吞噬所有低优先级的CPU时间片
线程被永久阻塞在一个等待进入同步块的状态
等待的线程永远不被唤醒
如何避免?
尽量不设置低线程优先级
子线程默认优先级和父线程一样,Java主线程默认的优先级是5
优先级范围为1-10
最低优先级(1)、正常优先级(5)、最高优先级(10)
Java作为跨平台语言,线程有10个等级,但是映射到不同操作系统的线程优先级值不一样
活锁
任何对象都可以作为锁,锁对象在对象头中
对象头信息
Mark Word,锁信息在这里面
Class Metadata Address
Array Length
偏向锁
每次获取锁和释放锁都会浪费资源
很多情况下,竞争锁不是由多个线程,而是一个线程在使用
轻量级锁
重量级锁
公平锁:
公平是针对锁的获取而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序
读写锁:
读写锁需要保存的状态
写锁重入的次数
读锁的个数
每个读锁重入的次数
使用多线程的原因:
提供程序的执行效率
好处:
1.可以把占据时间长的程序中的任务放到后台执行
2.运行效率可以提高
3.在一些等待的任务实现上使用
应用场景:
多线程下载
爬虫ipc
前端ajax
分布式job
进程是一个正在运行的应用程序,进程是线程的集合
线程是一个执行路径,一个执行单元
创建方法:
1.继承Thread类方法
2.实现runnable接口
3.使用匿名内部类
4.callable
5.线程池
同步和异步:
多线程的一致性
1.原子性
2.可见性
volatile
将当前缓存中的值立马写回主内存
写回内存的操作会使得其他CPU的缓存中的值立马失效
两条实现原则:
1.Lock指令会导致处理器缓存回写到主内存
2.一个处理器的缓存回写到主内存会导致其他处理器的缓存失效
final
final的可见性是指被final修饰的字段在构造器中一旦被初始化完成,那么其它线程中就能看见这个final字段了
synchronized
synchronized的可见性是在每次unlock之前都必须同步回主内存中
3.有序性
如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的
前半句是指串行的语义,而后半句是指指令重排序和工作内存和主内存的同步延迟
先行发生原则:
先行发生,是指操作A先行发生于操作B,那么操作A产生的影响能够被操作B感知到,这种影响包括修改了共享内存中变量的值、发送了消息、调用了方法等
1.程序次序原则
写在前面的先发生
2.监视器锁定原则
一个unlock的操作先行发生于同一个锁的lock操作
3.volatile原则
一个volatile的写操作先行发生于读操作
4.线程启动原则
对线程的启动优先于线程内所有的操作
5.线程终止原则
线程中的所有操作先行发生于检测到的线程终止
6.线程中断原则
对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否发生中断
7.对象终结原则
一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始
8.传递性原则
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
这里说的“先行发生”与“时间上的先发生”没有必然的关系
volatile的语义:
1.可见性
2.禁止重排序
底层主要靠内存屏障
synchronized并不是公平锁
形成死锁的条件:
1.互斥条件
资源是独占的且排他使用
2.循环等待条件
必须等待另一个进程放弃使用
3.不可剥夺条件
进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
4.请求和保持条件
进程每次申请它所需要的一部分资源,在申请新资源的同事,继续占有已分配的资源
并发编程模型的分类
1.内存共享
2.消息传递
java采用的是共享内存的方式
JMM:定义线程和主内存的关系
重排序:
编译器重排序
指令重排序
内存系统重排序
数据依赖性:
写后读
读后写
写后写
as-if-serial协议:
不管怎么排序,程序的执行结果不能被改变
把单线程程序保护了起来
程序顺序规则:
happens_before
顺序一致性:
数据竞争与顺序一致性保证
数据竞争:
1.在一个线程写变量
2.在另一个线程读同一个变量
3.写和读没有通过同步来排序
int类型占4个字节
boolean类型占1个字节
1.面向对象的特征:
四个基本特征:抽象,继承,封装,多态
封装:将对象封装成一个高度自治和相对封闭的个体
抽象:找出事物的相似性和共性之处,然后归结为一个类
继承:要实现一个功能时可以在已有的类的基础上来实现
多态:程序中定义的引用类型对应的具体类型
回答抽象的问题时要举例说明
2.有基本数据类型,为什么要包装类型
java是一个面向对象的语言,而基本的数据类型不具备面向对象的特性
原子性,可见性,有序性
happens-before原则(先行发生原则):
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
2.它会强制将对缓存的修改操作立即写入主存
3.如果是写操作,它会导致其他CPU中对应的缓存行无效
栈帧:
局部变量表
操作数栈
保存计算过程的中间结果,同时作为变量的临时存储空间
帧数据区
帧数据区中保存着访问常量池的指针
异常处理表
栈上分配
一项优化技术
线程私有的对象直接在栈上进行分配,在函数调用后自动销毁
栈上分配的技术基础是逃逸分析,逃逸分析的目的是看变量的作用域是否会逃逸出函数体
为什么要用补码:
1.0的补码全是0
2.容易计算
start()方法和run()方法的区别
1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。 2.run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。 记住:多线程就是分时利用CPU,宏观上让所有线程一起执行 ,也叫并发
Runnable接口和Callable接口的区别
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已; Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 判断线程是否执行完毕,Callable+Future/FutureTask可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。
CyclicBarrier和CountDownLatch的区别
CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行; - CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行 - CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务 - CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
volatile关键字的作用
多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据 - 代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。
获得线程dump
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,
检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程。
synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上: - ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 - ReentrantLock可以获取各种锁的信息 - ReentrantLock可以灵活地实现多路通知 另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。
ConcurrentHashMap的并发度
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap
ReadWriteLock
ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。 因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
FutureTask
FutureTask表示一个异步运算的任务,FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
Linux环境下如何查找哪个线程使用CPU最长
获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过 - top -H -p pid,顺序不能改变
这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号
使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,
从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环
最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了
Thread.sleep(0)作用
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。 既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。 如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
Java内存模型
Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容: - Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去 - 定义了几个原子操作,用于操作主内存和工作内存中的变量 - 定义了volatile变量的使用规则 - happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的
CAS
CAS,全称为Compare and Swap,即比较-替换。 假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。 当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
AQS
AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。 如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。 AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。 AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
Semaphore
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。 由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?
同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性 - CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成汇编代码执行的,汇编代码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。
CyclicBarrier:
内存屏障或者栅栏,构造函数传入拦截的线程数量以及一个拦截后调用的Runnable类,每次线程调用await方法就告诉CyclicBarrier,然后阻塞在那里,等全部
线程到达屏障,线程开始执行.
代码示例:
public class CyclicBarrierDemo {
static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Thread3());
static int num = 0;
static class Thread1 implements Runnable {
@Override
public void run() {
try {
num += 1;
Thread.sleep(5000);
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "-" + 1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
static class Thread2 implements Runnable {
@Override
public void run() {
try {
num += 1;
Thread.sleep(1000);
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "-" + 2);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
static class Thread3 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":num=" + num);
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Thread1(), "thread1");
Thread thread2 = new Thread(new Thread2(), "thread2");
thread1.start();
thread2.start();
System.out.println(Thread.currentThread().getName() + ":" + 0);
}
}
Semaphore:
信号量,限制同一时间,访问特定资源的线程数量,以保证合理的使用特定资源
代码示例:
public class SemaphoreDemo {
private static Semaphore semaphore = new Semaphore(3);
public static class Thread1 implements Runnable{
@Override
public void run() {
try {
semaphore.acquire();
Thread.sleep(1000);
System.out.println(UUID.randomUUID().toString());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 12; i++) {
new Thread(new Thread1()).start();
}
}
}
CAS:
AtomicInteger
代码示例:
public class AtomicDemo {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static int num = 0;
static class Thread1 extends Thread{
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
static class Thread2 extends Thread{
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread1().start();
new Thread2().start();
}
Thread.sleep(2000);
System.out.println(num);
System.out.println(atomicInteger.get());
}
}
AtomicStampedReference:
通过版本号解决ABA问题
原子性的保证:
1.总线锁
2.缓存锁
有两种情况不会使用
1.内容无法被缓存到缓存处理器或者缓存跨多个缓存处理器
2.有些处理器无法支持缓存锁定
java实现原子操作的方式
1.锁
2.cas循环实现原子操作
问题:
a.ABA问题
b.循环时间长开销大
c.只能保证一个共享变量的原子操作
jdk提供了AtomicReference来对引用对象进行处理
发布对象和对象逸出:
发布对象:使一个对象能够被当前范围之外的代码所使用
完全发布对象:
1.在静态初始化函数中初始化一个对象引用
2.将对象的引用保存到volatile类型域或者AtomicReference对象中
3.将对象的引用保存到某个正确构造对象的final类型域中
4.将对象的引用的保存到一个由锁保护的域中
对象逸出:一种错误的发布.当一个对象还没有构造完成时,就使它被其他线程可见
不可变对象
对象创建以后其状态就不能修改
对象所有域都是final类型
对象是正确创建的(在对象创建期间,this引用没有逸出)
1)所有成员变量必须是private
2)最好同时用final修饰(非必须)
3)不提供能够修改原有对象状态的方法
最常见的方式是不提供setter方法
如果提供修改方法,需要新创建一个对象,并在新创建的对象上进行修改
4)通过构造器初始化所有成员变量,引用类型的成员变量必须进行深拷贝(deep copy)
5)getter方法不能对外泄露this引用以及成员变量的引用
6)最好不允许类被继承(非必须)
缓存命中率影响因素
业务场景和业务需求
缓存的设计(粒度和策略)
缓存容量和基础设施
工具
Guava Cache
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。