Java多线程疑难点
进程和线程
Java的多线模型JMM问题可以总结为 2 个核心,3个要点
2个核心:主内存,工作内存缓冲区 重点 主内存和 工作内存缓冲区的数据不一致问题,原因是工作内存缓冲区是线程私有的,数据更新后,同步到主内存有时间差,而另外一个原因重排序,编译器会对指令进行优化重拍。
3个要点:原子性,可见性,有序性(happen-before) happen-before原则保证了某些指令必须在前面或者后面,指令重排有限制。
进程是计算机分配内存资源的基本单位,它是程序的一次运行的结果。
线程是进程的子集,线程是计算机调度的基本单位,进程内通过调度不同的线程执行不同的任务,线程共同分享进程的内存资源。
Java线程的状态,
wait,sleep,join,等方法会让线程进入等待状态或者超时等待状态
线程的阻塞状态,由线程的等待,超时等待,等待通知,IO请求,等待锁等引起的。
Java的锁
Java使用类的对象头,栈帧的锁记录 即为实现锁机制。
对象头拥有 锁状态,hashcode,年龄标记,偏向锁 字段
Java中使用多线程的方式
- 继承Thread类,实现run方法
- 实现Runner接口,实现run方法
- 实现Callable接口,实现call方法,返回值是一个Future接口,Future接口可以控制线程的活动状态和取得方法结果
start方法会在当前线程内创建一个新的线程,run方法是线程执行的方法体,如果在当前线程内调用run,不会启动一个新的线程。
线程的常用方法
isAlive()
sleep()
getId()
Thread.currentThread()
疑难点:线程的中断
中断方法this.interrupt() 将线程的打上中断标识,但是线程并不会停止
this.interrupted 静态方法,判断当前线程是否中断,副作用是把线程的中断标识抹去
this.isInterrupted 判断线程是否中断,没有副作用
疑难点: 线程如何停止
常用的方法是用 没有副作用的中断方法 isInterrupted判断 后,如果为真,那么抛出一个中断异常 throw new InterruptedException();
当然也可以采用 return ; 的方式 让线程停止,但是不推荐这种方式,因为用异常的方式,可以将异常抛出给上一层捕捉,代码的体验更好。
在sleep后,执行线程的中断方法interrupt()
在sleep状态下,停止某一个线程会抛出中断异常,并清除中断标识
被忽略的方法 yield 方法
这个方法的作用是让正在执行的线程放弃CPU资源,让给其他的线程。但是线程的执行是不可预知的,有可能刚刚放弃CPU,马上又获得了CPU。
守护线程
setDaemon
对象和变量的并发访问
并发:多个线程在一段时间间隔内对共享临界资源的访问,这个可能导致脏读。如果一个线程在读取一个数据量,而另外的一个线程同时在修改这个数据量,这就会导致当前线程读取的数据是修改前的,不是最新的。
解决的办法就是 同步,但是在同步之前,需要知道一点,那就是只有对共一个资源的访问 才会导致多线程的 竞争,而在Java中 的临界资源可以是 一个相同的变量,一个相同的引用对象,一个相同的引用对象的方法,等。
Java中锁的相关概念:
每一个实例对象都有一个锁,但是不同的实例对象有不同的锁。
public class Task implements Runner{ public Object cls; public Task(Object target){ this.cls=target; } public void run(){ cls.method(); } }
public class TaskB implements Runner{
public Object cls;
public TaskB(Object target){
this.cls=target;
}
public void run(){
cls.methodB();
}
}
public class Has{ public synchronized void method(){ System.out.println("task"); }
public void methodB(){
System.out.println("taskB");
}
} public class Test{ public static void main(String[] args){ Has a=new Has(); Has b=new Has(); Thread thread1=new Thread(new Task(a)); thread1.start(); Thread thread2=new Thread(new Task(b)); thread2.start(); } }
/*
这个实例中并不会发生竞争,因为线程1和线程2 想要获得对象锁分别是 a,b的对象锁。
但是在下面的改写的代码中就会发生竞争,因为此时线程1和线程2想要获得的锁 都是 对象a的监视器,临界资源就是实例对象a.method方法
Thread thread1=new Thread(new Task(a));
thread1.start();
Thread thread2=new Thread(new Task(a));
thread2.start();
在这种情况下,线程1和线程2 也不会发生竞争,因为线程2想要执行的是对象a.methodB方法,进入该方法不需要获取对象a的锁,所以和线程1没有竞争关系
Thread thread1=new Thread(new Task(a));
thread1.start();
ThreadB thread2=new ThreadB(new Task(a));
thread2.start();
*/
使用synchronized 方法,运行的原理是 “对象监视器 ”
JVM会通过对象监视器 判断当前线程有没有持有 锁,如果当前线程持有锁,那么线程就能进入临界区,没有则需要等待持有锁。
同步代码块
synchronized(this) 要求持有当前对象的锁
synchronized(Object) 要求持有非this对象,Object对象的锁,一般不会用字符串的字面量作为Object,因为字符串的字面量 "aaa "可以让当前同步代码块一直持有锁,而不释放。
同步方法和同步代码块,要求多线程在一段时间间隔内访问 同一实例对象 的同步资源时,要求持有锁,才能访问,但是如果访问的 同一实例对象的 非同步资源 时,那么多线程将会交替执行。
锁的重入
当一个线程获得该个对象锁时,再次请求获得该个对象的锁是可以获得锁的。
当存在父子继承关系时,子类可以通过“可重入的锁”调用 父类同步方法。
同步不能继承
父类拥有同步方法A,子类继承方法A,但是子类的方法A没有同步功能,还需要添加修饰符synchronized
异常发生,线程释放锁
如果两个线程A,B竞争同一个对象锁,线程A获得对象锁,线程B进入等待队列,线程A在执行过程中发生异常,线程A释放锁,线程B获得对象锁,继续执行线程B的任务。
多线程的死锁:
可以通过Java的命令行工具
jsp检测进入死锁的进程
然后用jstack -l id 查看发生死锁的线程
类锁 对象锁
class.this 对所有的类对象实例都有效
this 都同一个对象实例有效
public class Task implements Runner{ public Object cls; public Task(Object target){ this.cls=target; } public void run(){ cls.method(); } } public class TaskA implements Runner{ public Object cls; public TaskA(Object target){ this.cls=target; } public void run(){ cls.methodA(); } } public class Has{ public void method(){ synchronized(this){ System.out.println("task"); } } public synchronized static void methodA(){ System.out.println("taskA"); } } public class Test{ public static void main(String[] args){ Has a=new Has(); Has b=new Has(); Thread thread1=new Thread(new TaskA(a)); thread1.start(); Thread thread2=new Thread(new TaskA(b)); thread2.start(); Thread thread3=new Thread(new Task(b)); thread2.start(); } }
/*
在这个情况下,线程1和线程2会发生竞争,而线程3会异步执行。
因为线程1和线程2想要获得的锁是class锁,Has.this,即使线程1和线程2操作的对象是a和b,但是它们执行的方法是类方法,又被synchronized修饰,所以进入类方法前必须要获得has.this锁
线程3获得锁是对象b的this锁,和线程2想持有的锁不是同一把,所以线程交替执行。
*/
关键字volatile
强制线程每次从共享内存读取变量,而不是私有内存读取变量。
写操作 永远 在读操作的前面执行
使用内存屏障,实现语义
store store barrier
volatile 写
store load barrier
volatile 读
load load barrier
load store barrier
final 赋值时机
1 类变量 静态代码块,声明
2 实例变量 声明,constructor, 非静态代码块
synchronized的两个特性:互斥性 可见性
在同一个时刻,只有一个线程执行同步方法,同步代码块,而且能让这个线程 看见 上一个线程 修改后的对象的状态。
等待,通知流程 实现线程间的通信
wait() 当前线程进入等待状态,释放锁
wait(long) 当前线程进入等待状态,释放锁,时间到达自动唤醒,尝试获取锁
notify() 线程规划器随机挑选一个呈wait状态的线程,对其发出通知,但是此时wait状态的线程要等待通知线程执行完毕,释放锁后,才尝试获取锁
notifyAll() 通知全部的等待线程
wait方法执行完毕后要释放锁,而notify执行完毕后,要当前线程执行完后,才释放锁
以上的方法,都需要在同步方法或者同步代码块内执行,因为如果当前线程没有获取锁,就执行以上的方法,会抛出没有对象监视器的异常 IllegalMonitorStateException
线程状态的细分
可运行Runnable 调用start方法,这是准备阶段,系统分配CPU资源
正在运行Running 调用run方法,线程抢到CPU资源
线程阻塞Blocked
线程进入Runnable状态,线程暂停
- 调用sleep后,超过指定的休眠时间
- 线程调用的阻塞IO操作已经返回,阻塞方法执行完毕
- 线程成功地获取试图同步的监视器
- 线程正在等待某个通知,其他线程发出了通知
每个锁对象都有两个队列,一个就绪队列,一个阻塞队列,就绪队列存储了即将获得锁的线程。一个线程被唤醒后进入就行队列;一个线程进入wait状态,进入阻塞队列。
在使用等待通知机制时,要注意如果 过早通知,那可能让等待线程进入无限期的阻塞;判断wait方法的逻辑混乱,程序可能出现异常。
管道流
线程间直接进行通信
PipeInputStream PipeOutputStream
PipeReader PipeWriter
线程通信更加简便的方法:
- 使用阻塞队列
- 使用同步队列,同步哈希
- 使用同步工具
方法join
这个方法可以让一个线程等待另一个线程执行完毕后,才会往后执行。
join方法使得自己所属线程正常执行,而使得当前线程进入无限期的阻塞,知道所属线程执行完毕。
join方法 使得线程具有排队运行的作用。
join 和 synchronized 区别
join 方法 内部使用 wait() 进行等待
synchronized 关键字 使用的是 “对象监视器” 原理作为同步
join(long) 内部使用的是wait(long) 实现,所以它会释放锁
sleep(long) 不会释放锁
ThreadLocal 类
为每个线程创建public static 变量,用不同的盒子,创建不同线程的私有变量
解决get 方法 返回null 的异常,继承ThreadLocal 类,重写 get 方法,如果返回为空,自带创建一个对象,同时用set方法存储。
InheritableThreadLocal类
值继承,可以让子线程从父线程中获取值,当然也能修改值。
fork框架
Lock对象
ReentrantLock类
ReentrantReadWriteLock类
Condition 通知类
嗅探锁定,多分支通知
一个ReentrantLock类对象实例,可以生成多个Condition类对象实例,
Condition对象的 await signal signalAll 方法
公平锁和非公平锁
公平锁是按照线程加锁的顺序分配锁的,FIFO先进先出的顺序;非公平锁是抢占式的
new ReentrantLock(false) 非公平锁,默认公平锁
定时器Timer 和 抽象任务类TimerTask
new Timer() 就是启动一个新的线程,它会一直运行下去,但不是守护线程
new Timer(true) 以守护线程的方式启动
任务类要继承TimerTask抽象类,重写run方法
定时器方法schedule
schedule(TimerTask task, Date time) 在计划时间到达后,执行任务
schedule(TimerTask task, Date firstTime, long period) 在计划时间执行任务后,每个一段时间重复执行任务
schedule(TimerTask task, long delay) 在线程启动后,delay间隔后执行任务
schedule(TimerTask task, long delay, long period)
如果计划执行任务的时间 晚于 当前时间,那么任务在未来执行
如何计划执行任务的时间 早于 当前时间,线程启动后立马执行任务
TimerTask是以队列的方式一个一个被执行的,如果前面一个任务延迟了,那么后面的任务执行的时间也会被延迟
TimerTask 类的cancel 方法 是把 任务从队列中 清除
Timer类的cancel方法是把 当前队列的所有 任务清除,但有时候起不到效果
scheduleAtFixedRate(TimerTask task, Date firstTime , long period)
这个方法具有追赶性,延时执行的任务会在后面插入执行,而普通的schedule方法被延时的任务不会执行。
单例模式 与 多线程
使用静态内部类,enum类,或static 代码块创建单例模式
线程的状态
Thread.State 线程状态的枚举类
getState方法 能获取线程的状态
线程组
一级关联:父对象中有子对象,但是并不创建子孙对象,这种模式在开发中非常常用。(通常是创建一个线程组,然后把部分线程归属这个线程组,进行统一管理)
多级关联:
一级关联实例:
public class Task implements Runner{ public void run(){ System.out.println("this thread "+Thread.currentThread().getName()); } } public class Test{ public static void main(String[] args){ Thread t1=new Thread(new Task()); Thread t2=new Thread(new Task()); ThreadGroup g=new ThreadGroup("a group thread"); Thread a=new Thread(g,t1); Thread b=new Thread(g,t2); a.start(); b.start(); System.out.println(g.activeCount()); /**
线程a的运行结果和线程t1相同
*/ } }
线程中出现异常的处理方式
UncaughtExceptionHandler类
线程对象拥有一个setUncaughtExceptionHandler的方法,通过该个方法可以处理线程发生的异常。
静态方法setDefaultUncaughtExceptionHandler可以为所有线程对象设置异常处理器。
线程组可以通过继承ThreadGroup类,实现uncaughtException方法处理异常。
线程异常处的递归
- 如果线程实例对象设置了异常处理器,那么对象的异常处理器先处理 异常
- 如果没有,那么如果线程的静态方法,设置了异常处理器,那么静态异常处理器处理异常
- 如果没有,那么线程组的异常处理方法 处理异常
- 如果没有,线程 抛出异常