参考:
https://www.cnblogs.com/goody9807/p/6522176.html
背景:
线程创建的两种方法:
1. 继承runnable接口传入Thread
实例对象作为单实例传递给Thread 所以创建多个线程共用一个实例对象,里面的属性也都是共享的。
2. 继承Thread
多线程工作原理:
线程1:操作步骤–工作内存–总内存。
线程2:操作步骤–工作内存–总内存。
线程3:操作步骤–工作内存–总内存。
流程:
每个线程从总内存种读取内存到工作内存,然后在操作步骤中对共享数据进行操作,将修改后的数据更新到工作内存,再更新到总内存。
方法一:validate :针对工作内存–总内存
修饰共享变量,保持数据可见性,适用范围:只读取,不修改。
意思:当多线程访问共享数据的时候只是读取,没有修改,这个时候就可以使用validate。
只需在共享数据前面加上volatile修饰,即可实现线程同步。
方法二,三:synchronized同步方法和同步块
这两种方式都要用到synchronized关键字。
synchronized:锁住代码块,保持顺序性。
意思就是 使得线程内是顺序执行,线程间是随机。
流程:
每个对象都有一个“锁标志”,当这个对象的一个线程访问这个对象的某个synchronized 数据时,这个对象的所有被synchronized 修饰的数据将被上锁(因为“锁标志”被当前线程拿走了),
只有当前线程访问完它要访问的synchronized 数据时,当前线程才会释放“锁标志”,这样同一个对象的其它线程才有机会访问synchronized 数据。
synchronized 关键字用于保护共享数据
目的是使同一个对象的多个线程,在某个时刻只有其中的一个线程可以访问这个对象的synchronized 数据。
所以尽量只给操作共享数据的代码块加synchronized锁一般使用同步块,锁的内容少,开销小。
方法四:重入锁ReentrantLock
链接:重入锁:ReentrantLock 详解
链接:线程调度之ReentrantLock实现 多线程顺序执行任务
ReentrantLock() : 创建一个ReentrantLock实例
ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
lock() : 获得锁
unlock() : 释放锁
注:关于Lock对象和synchronized关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
lock.lock(); // 看这里就可以
//lock.lock(); ①
try {
i++;
} finally {
lock.unlock(); // 看这里就可以
//lock.unlock();②
}
}
}
注意:需要手动来释放重入锁
方法五:局部变量实现线程同步(非阻塞型)
使用局部变量实现线程同步:采用以”空间换时间”的方法,与前面方法采用的”时间换空间”不同
ThreadLocal:
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
题外话:线程的释放锁重点内容
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。