先复习下
我们先了解一下Thread的几个重要方法。
a、start()方法,调用该方法开始执行该线程;
b、stop()方法,调用该方法强制结束该线程执行;
c、join方法,调用该方法等待该线程结束,用于主线程等待子线程执行结束。
d、sleep()方法,调用该方法该线程进入等待。
e、run()方法,调用该方法直接执行线程的run()方法,但是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!
自己的理解:
首先object提供了wait和notify,网上很多都讲得非常奇怪
这两个方法是必须要跟synchronize一起用的,否则会异常。
- wait方法:被锁的对象.wait()表示,正在使用该锁的线程,不再拥有该锁,所以线程也就停止,进入了等待notify唤醒的block区。
- notify:被锁的对象.notify()表示,在block区中找一个优先级高的线程唤醒,进入runnable,等待获得锁
所以,它和sleep是没有可比性的,sleep是操控线程,而它是操控对象
并发编程 锁
Java中的锁
- synchronized
- Lock
Lock接口最常用的:
1. ReentrantLock
/**
* 执行结果:
* 线程B得到了锁...
* 线程B释放了锁...
* 线程A得到了锁...
* 线程A释放了锁...
*/
public class LockTest {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
Lock lock = new ReentrantLock();
public static void main(String[] args) {
final LockTest test = new LockTest();
new Thread("A") {
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread("B") {
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
// 注意这个地方:lock被声明为局部变量
lock.lock();
try {
System.out.println("线程" + thread.getName() + "得到了锁...");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
} finally {
System.out.println("线程" + thread.getName() + "释放了锁...");
lock.unlock();
}
}
}
2. ReentrantReadWriteLock 读写锁
/**
* Thread-0正在进行读操作
* Thread-0正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-0正在进行读操作
* Thread-1读操作完毕
* Thread-0读操作完毕
* 读写锁并不会真正去检查你是否在读 是否在写
* 关键在于你怎么用它
*/
public class ReadLockTest {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private List<String> ls = new ArrayList<>();
public static void main(String[] args) {
final ReadLockTest test = new ReadLockTest();
final Thread thread = new Thread() {
public void run() {
test.get(Thread.currentThread());
}
};
final Thread thread1 = new Thread() {
public void run() {
test.get(Thread.currentThread());
}
};
thread.start();
thread1.start();
}
public void get(Thread thread) {
System.out.println(thread.getName());
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 30) {
System.out.println(thread.getName() + "正在进行读操作");
}
System.out.println(thread.getName() + "读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}
锁类型
-
可重入锁:在执行对象中所有同步方法不用再次获得锁
-
可中断锁:在等待获取锁过程中可中断
-
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
-
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
synchronized与Lock的区别
1、我把两者的区别分类到了一个表中,方便大家对比:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 、可判断 、可公平(两者皆可)、 可中断 |
性能 | 差不多 | 差不多 |
***** synchronized不可中断,所以性能有时会比较差,此时可以用Lock接口的ReentrantLock 用interrupt中断
多线程中断机制
在 java中启动线程非常容易,大多数情况下是让一个线程执行完自己的任务然后自己停掉。
一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果,比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作(阻塞、限期等待或者无限期等待状态),那么调用线程的 interrupt() 方法就无法使线程提前结束(如IO阻塞、sync操作)。
线程中断是一种协作机制,调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时间中断自己。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
before
after
after
很明显,相对于synchronized,Lock这种可以控制多个condition的方式,更加方便。
Java内存模型
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
主内存与工作内存
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
内存间交互操作
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
-
read:把一个变量的值从主内存传输到工作内存中
-
load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
-
use:把工作内存中一个变量的值传递给执行引擎
-
assign:把一个从执行引擎接收到的值赋给工作内存的变量
-
store:把工作内存的一个变量的值传送到主内存中
-
write:在 store 之后执行,把 store 得到的值放入主内存的变量中
-
lock:作用于主内存的变量
-
unlock
内存模型三大特性
1.原子性
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性
有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
2. 可见性
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
3. 有序性
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
J.U.C - AQS
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
CountdownLatch
用来控制一个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
run..run..run..run..run..run..run..run..run..run..end
volatile关键字
1.volatile保证可见性
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
-
第一:使用volatile关键字会强制将修改的值立即写入主存;
-
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
-
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
2.volatile不能确保原子性
3.volatile保证有序性
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
-
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
-
在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
123456789 | |
有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
volatile的应用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
下面列举几个Java中使用volatile的几个场景。
①.状态标记量
123456789 | |
根据状态标记,终止线程。
②.单例模式中的double check
1234567891011121314151617 | |