Java 多线程常见问题学习与整理
线程的状态:
1.NEW(新建)
2.RUNNABLE(准备就绪)
3.BLOCKED (阻塞)
4.WAITING (不见不散)
5.TIMED_WAITING (过期不候)
6.TERMINATED (终结)

wait和sleep:
1.wait是Object类的方法 任何对象都可以调用 。
sleep是Thread的静态方法
2.sleep不会释放锁(线程被阻塞了) wait会释放锁
3.都会被interrupted方法中断
管程(锁)
又叫Monitor(监视器) 也是我们常说的锁
是一种同步机制 保证同一时间内只有一个线程能够访问我们被保护的对象
用户线程 和 守护线程:
用户线程:平时我们自定义的线程
守护线程:一种特殊线程 运行在后台 比如垃圾回收线程 可以通过线程对象的setDaemon(true)方法将线程设为守护线程
JVM的生命周期取决于用户线程。当存在用户线程时 JVM保持运行。若不存在用户线程时,即使存在守护线程,JVM结束。
创建线程的四种方式
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
4.线程池
多线程编程步骤
1.创建资源类,资源类有属性和方法
class Ticket{
private Integer num;
public Ticket(Integer num){
this.num=num;
}
public synchronized void sold(){
if(num>=1){
num--;
System.out.println(Thread.currentThread().getName()+"卖票"+"当前剩余"+num);
}
}
}
2.创建多个线程,调用资源类的操作方法
Ticket ticket = new Ticket(30);
//创建三个线程卖票
for(int i=0;i<3;i++){
Thread thread = new Thread(() -> {
for(int j=0;j<20;j++){
ticket.sold();
}
});
thread.start();
}
问题:当我们调用Thread的start方法后,是否会立即创建线程?
答:不一定,因为我们查看源码可以发现其调用了一个native方法private native void start0(); 该方法将线程交给操作系统创建,操作系统不一定立即创建线程
Lock接口
Lock需要手动上锁 解锁
可重入锁:ReentrantLock
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
class Ticket{
private Integer num;
public Ticket(Integer num){
this.num=num;
}
private final ReentrantLock lock=new ReentrantLock();
public void sold(){
//上锁
lock.lock();
//使用try-finall语句确保 不因异常导致没有解锁
try{
if(num>=1){
num--;
System.out.println(Thread.currentThread().getName()+"卖票"+"当前剩余"+num);
}
}finally {
//解锁
lock.unlock();
}
}
}
Lock与synchronized的不同:
1.Lock是接口,synchronized是JAVA的关键字,synchronized是内置实现
2.Lock的实现类ReentrantLock是可重入锁,默认为非公平锁,可以传入参数true实现公平锁。synchronized是非公平锁,可重入锁。
3.synchronized在发生异常时会自动释放锁,而Lock在发生异常时 如果没有手动执行unlock()方法则不会释放锁,因此使用Lock需要在finally语句中释放锁
4.Lock可以让等待锁的线程响应中断(lock.lockInterruptibly()),而synchronized却不行,使用synchronized时 等待的线程会一直等待下去不能响应中断
5.通过Lock可以知道有没有获取锁,而synchronized却不行
6.Lock可以提高多个线程进行读操作的效率
7.Lock提供了非阻塞获取锁的方式(trylock()没有得到锁会返回false)
从性能上说 如果竞争不激烈的情况下两者的性能是差不多的,当竞争特别激烈时(有大量线程同时竞争),Lock的性能要远远优于synchronized
可重入锁的原理
每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态,如果锁状态是0,代表该锁没有被占用,直接进行CAS操作获取锁,将线程ID替换成自己的线程ID。如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法。如果是非重入锁,就会进入阻塞队列等待。
释放锁时,可重入锁,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
释放锁时,非可重入锁,线程退出方法,直接就会释放该锁。
线程间的通信
控制线程的执行顺序
1.创建资源类,资源类有属性和方法
2.在资源类中操作方法
(1)判断
(2)干活
(3)通知
class Ticket{
private Integer num=0;
public Ticket(){
}
public synchronized void incr() throws InterruptedException {
while(num!=0){ //如果num不为0,等待
this.wait();
}
num++;
//通知其他线程
this.notify();
}
public synchronized void decr() throws InterruptedException {
while(num!=1){ //如果num不为1,等待
this.wait();
}
num--;
//通知其他线程
this.notify();
}
}
或者
class Ticket{
private Integer num=0;
private final condition=lock.newCondition();
private final condition2=lock.newCondition();
public Ticket(){
}
public synchronized void incr() throws InterruptedException {
while(num!=0){ //如果num不为0,等待
condition.await();
}
num++;
//唤醒因condition.await()沉睡的线程
condition.signalAll();
}
public synchronized void decr() throws InterruptedException {
while(num!=1){ //如果num不为1,等待
condition2.await();
}
num--;
//唤醒因condition2.await()沉睡的线程
condition2.signalAll();
}
}
3.创建多个线程,调用资源类的操作方法
wait,notify/notifyAll
1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 锁对象.wait()、notify/notifyAll() 方法。
3、由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待该锁状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次
释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
- notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理
的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。
比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
注意我们为什么用while进行循环判断而不是if?
原因在于wait()方法是在哪里沉睡就在哪里唤醒,如果采用if进行判断(判断只执行一次) 则 当线程被唤醒时 没有再次对条件进行判断!(虚假唤醒问题)
线程间定制化通信
是线程按照约定的顺序执行
比如 AA线程执行五次 BB线程执行五次 CC线程执行五次
思路就是通过标志位flag 判断当前线程是否沉睡
ArrayList并发安全问题
ArrayList是并发不安全的,当我们并发修改其中数据时 会发生ConcurrentModificationException异常
解决方案:
1.Vector:实质就是方法上添加了synchronized关键字
2.Collections.synchronizedList(ListSynchronizedList,这个类的方法内通过synchronized加锁
3.JUC中有一个类CopyOnWriteArrayList 代替 ArrayList(主要解决方法)。该类在读的时候可以并发读 在写的时候必须独立写。在每次写的时候会复制一份CopyOnWriteArrayList,再往里写东西,然后再合并回原来的CopyOnWriteArrayList(整个过程是加锁的)
HashSet和HashMap并发安全
HashSet:CopyOnWriteArraySet
HashMap:ConcurrentHashMap
synchronized的三种情况
- public void synchronized
此时锁为 调用方法的对象 - public static void synchronized
此时锁为 类.class - synchronized( xxx){
}
此时的锁为 括号里的对象
公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
ReentrantLock reentrantLock = new ReentrantLock(true);
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
死锁
线程1持有锁A,想要获取锁B。线程2持有锁B,想要获取锁A。 此时产生死锁
验证是否死锁:
(1)jps -l 获取进程号

(2)jstack 进程号 :进行堆栈跟踪


** Callable接口 **
Callable接口与Runnable接口:
- Callable接口的方法为call() 有返回值,如果没有返回值会抛出异常
- Runnable接口的方法为run() 无返回值
//泛型应该是返回值的类型
FutureTask<Integer> futureTask = new FutureTask<>(()->{return 1024;});
new Thread(futureTask).start();
//通过get()方法获得返回值
System.out.println(futureTask.get());
乐观锁和悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
读锁和写锁
读锁:共享锁
写锁:独占锁
CAS
compare and swap

ABA问题:其他线程经过多次修改后,值和修改前的值相同。解决方法就是 增加一个版本号变量,当值被修改后版本号也必然发生改变
CAS的底层在linux下就是lock cmpxchg指令(汇编语言)需要注意的是cmpxchg指令不能保证原子性(因为可能在比较完后数据被其他线程修改) 保证原子性的是lock指令。
synchronized锁升级
new出对象 --> 偏向锁 --> 轻量级锁 (无锁 /自旋锁)--> 重量级锁
对象被new出后,第一次上锁为偏向锁,有人争用升级为轻量级锁,如果竞争激烈升级为重量级锁。这些信息都记录在对象的对象头中(markword)

无锁 -> 偏向锁 : 只是在对象中记录 锁的拥有者
偏向锁 -> 轻量级锁 : 首先撤销偏向锁 状态, 在争抢锁的线程栈中创建一个Lock Record记录 ,采用自璇(CAS)的方式争抢锁,锁对象记录拥有者的Lock Record
轻量级锁 -> 重量级锁 :如果锁竞争激烈 或者 有线程自璇过多,那么就会升级为重量级锁。需要向操作系统申请资源,没有抢到锁的线程挂起进入等待队列
锁降级:发生在gc 此时只有gc线程访问,锁降级本质没有意义
锁消除 lock eliminate
我们看下面一段代码
public synchronized void add(String s1,String s2){
StringBuffer sb=new StringBuffer();
sb.append(s1).append(s2);
}
我们都知道StringBuffer是线程安全的,原因就在于他的方法上有synchronized关键字,由于此时sb是一个局部变量(栈私有),不是共享资源,它不可能被其它线程访问,所以add方法上加锁没有必要,此时编译器会进行锁消除
锁粗化 lock coarsening
public void test(String s1){
int i=0;
StringBuffer sb=new StringBuffer();
while(i<100){
sb.append(s1);
i++;
}
JVM检测到在while内对同一个对象加100次锁,此时JVM会将加锁的范围粗话到while这一个虚幻体外,使得只用加一次锁

浙公网安备 33010602011771号