多线程学习
(一)线程的介绍
一、创建线程的方式
1. 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(" hello java! " );
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2. 实现Runnable结构
public class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("hello java! ");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
二、 工作线程和守护线程
工作线程 默认创建的线程 main线程
守护线程 后台运行的线程 jvm后台线程
工作线程结束以后 守护线程会一起结束
三、 父线程和线程组
每个线程都有一个父线程的概念,就是在哪个线程里创建这个线程,
那它的父线程就是谁。
线程组 对多个线程进行管理
enumerate():复制线程组里的线程
activeCount():获取线程组里活跃的线程
getName()、getParent()、list(),等等
interrupt():打断所有的线程
destroy():一次性destroy所有的线程
默认线程会加入父线程的ThreadGroup
四、线程优先级
设置线程优先级,理论上可以让优先级高的线程先执行
实际上CPU不一定按优先级高的先执行
优先级一般设置在1-10之间, 1为最低 5为默认 10为最高
创建线程默认继承父线程的优先级
线程的优先级不能大于线程组的优先级
五、 线程初始化
1. 创建你的线程,就是你的父线程
2. 如果没有置顶ThreadGroup, 线程的ThreadGroup就是父线程的ThreadGroup
3. 线程的daemon状态默认是父线程的daemon状态
4. 线程的优先级默认是父线程的优先级
5. 如果没有执行线程的名称,默认就是Thread-0格式的名称
6. 线程的id是全局递增的,从1开始
六、 线程的启动
1. 一旦启动了线程之后,就不能再重新启动了,多次调用start()方法,因为启动之后,
ThreadStatus就是非0的状态了,此时就不能重新调用了
源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
2. 启动线程之后,这个线程就会加入之前处理好的那个线程组中
3. 启动一个线程实际上走的是native方法,start0(), 会实际的启动一个线程
4. 一个线程启动之后就会执行run()方法
七、 线程休眠 Thread.sleep()
JDK 1.5之后就引入了TimeUnit这个类
TimeUnit.HOURS.sleep(1)
TimeUnit.MINUTES.sleep(5)
TimeUnit.SECONDS.sleep(30)
TimeUnit.MILLISECONDS.sleep(500)
八、 线程让度 Thread.yield()
担心说某个线程一直长时间霸占着CPU,导致其他的线程很少得到机会来执行,所以设计了
一个yield方法,你调用之后,可以尝试说当前线程先别执行了,CPU可以去执行其他线程了
这个方法常见于debug和test场景下的程序
在这样的一些场景下,他可以复现因为锁争用导致的一些bug
他也可以用于设计一些并发控制的工具,比如说在java.util.concurrent.locks包下的一些类
九、 线程加入 thread.join()
线程A里面开启了一个线程B,线程A如果对线程B调用了join方法,就会导致线程A阻塞,等待
线程B逻辑执行结束后,才会继续执行线程A
十、 线程中断 thread.interrupt()
修改标志位
interrupt打断一个线程,其实是在修改那个线程里的一个interrupt的标志位,打断他以后,
interrupt标志位就会变成true,所以在线程内部,可以根据这个标志位,
isInterrupted这个标志位来判断,是否要继续运行
并不是说,直接interrupt一下某个线程,直接就不让他运行了
(二)volatile
一、 CPU的内存模型
1. 总线加锁(已淘汰)
2. CPU的 MESI 缓存一致性协议(指令) 变量值修改 其他CPU中缓存立马过期
CPU的 嗅探机制 嗅探发现变量过期,立马重新读取变量值
内存屏障 lock前缀指令 -> 内存屏障
二、 Java内存模型
read(从主存读取)
load(将主存读取到的值写入工作内存)
use(从工作内存读取数据来计算)
assign(将计算好的值重新赋值到工作内存中)
store(将工作内存数据写入主存)
write(将store过去的变量值赋值给主存中的变量)
三、 并发编程过程中,可能会产生的三类问题
1. 可见性
线程间对变量的修改不可见 volatile保证可见性
2. 原子性
i++ 非原子性操作 volatile不保证原子性
3. 有序性
对于代码,同时还有一个问题是指令重排序,编译器和指令器,
有的时候为了提高代码执行效率,会将指令重排序 volatile保证有序性
四、volatile(保证线程间的可见性,不保证原子性,保证有序性)
1. 保证可见性
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,
CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,
所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改
如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,
然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了
2. 保证有序性
java中有一个happens-before原则:
编译器、指令器可能对代码重排序,乱排,要守一定的规则,happens-before原则,
只要符合happens-before的原则,那么就不能胡乱重排,如果不符合这些规则的话,
那就可以自己排序
内存屏障 禁止重排序
四种屏障
LoadLoad屏障:
Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,
他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的
StoreStore屏障:
Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,
先于Store2以及后续指令
LoadStore屏障:
Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令
StoreLoad屏障:
Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,
先于Load2以及后续指令的数据装载
(三)synchronize
一、 加锁
synchronized可以对两种对象加锁,对象实例,Class对象
对类加锁,也是在针对一个对象实例进行加锁,其实他的意思就是对那个类的Class对象进行加锁
synchronized一个静态方法,就是对这个类的Class对象加锁
二、 monitor
monitorenter指令
每个对象都有一个关联的monitor,比如一个对象实例就有一个monitor,
一个类的Class对象也有一个monitor,如果要对这个对象加锁,
那么必须获取这个对象关联的monitor的lock锁
他里面的原理和思路大概是这样的,monitor里面有一个计数器,从0开始的。
如果一个线程要获取monitor的锁,就看看他的计数器是不是0,如果是0的话,
那么说明没人获取锁,他就可以获取锁了,然后对计数器加1
这个monitor的锁是支持重入加锁的,什么意思呢,好比下面的代码片段
synchronized(myObject) {
// 一大堆的代码
synchronized(myObject) {
// 一大堆的代码
}
}
monitorexit指令
如果一个线程第一次synchronized那里,获取到了myObject对象的monitor的锁,计数器加1
然后第二次synchronized那里,会再次获取myObject对象的monitor的锁,这个就是重入加锁,
然后计数器会再次加1,变成2这个时候,其他的线程在第一次synchronized那里,会发现说
myObject对象的monitor锁的计数器是大于0的,意味着被别人加锁了,然后此时线程就会
进入block阻塞状态,什么都干不了,就是等着获取锁 接着如果出了synchronized
修饰的代码片段的范围,就会有一个monitorexit的指令,在底层。
此时获取锁的线程就会对那个对象的monitor的计数器减1,
如果有多次重入加锁就会对应多次减1,直到最后,计数器是0
三、 线程间的通信 wait和notify/nofifyAll
wait与sleep的区别:前者释放锁,后者不释放锁
wait(),必须是有人notify唤醒他
wait(timeout),阻塞一段时间,然后自己唤醒,继续争抢锁
wait与notify,必须在synchronized代码块中使用,因为必须是拥有monitor lock的线程才可以执行wait与notify操作
因此wait与notify,必须与synchornized一起,对同一个对象进行使用,
这样他们对应的monitor才是一样的
notify()与notifyall():前者就唤醒block状态的一个线程,后者唤醒block状态的所有线程