本章学习内容:Synchronized、Lock、Volatile
锁的出现就是为了保护共享资源的独占性,避免多个线程同时操作一个共享资源
学习之前,先来了解三个锁的区别:
java内存模型
- 每个线程都有自己的本地内存空间(即jvm栈中的帧),线程执行时,先把主内存的数据拷贝到工作内存,同步代码结束后,再把工作内存中的数据更新到主内存中,这样就能够保证并发场景下,主内存中的共享变量数据的一致性;
- 锁的两种特性:
- 互拆性(mutual exclusion):互拆即一次只允许一个线程持有某个特定的锁;
- 可见性(visibility):一个线程修改了变量其它线程可以立马知道;
- java内存模型三种特性:
- 原子性(Atomicity):原子不可分割,原子操作不可中断(要么成功,要么失败,即使多线程下,一个操作一旦开始,不受其它线程干扰)。java的concurrent包下提供了一些原子类(AtomicInteger、AtomicLong、AtomicReference);
- 可见性(Visibility):一个线程修改了共享变量值,其它线程能够得知这个修改,其它线程对这个修改是可见的(实现:java内存模型即以主内存为传递媒介方式实现可见性——变量修改后将新值同步到主内存,在变量读取前从主内存刷新变量值);
- 有序性(Ordering):java提供关键字Volatile和Synchronized来保证线程之间操作的有序性,即保证变量赋值操作的顺序与程序代码中的执行顺序一致(好比:a=1,b=2。按照代码顺序是先对a赋值再对b赋值,但由于指令重排序,很可能b就先赋值了,所以普通变量不能够保证有序性。而Volatile可以禁止指令重排序。而Synchronized根据其规则”一个变量在同一时刻只允许一个线程对其进行锁操作”决定了持有同一个对象锁的两个或多个同步块只能串行执行);
synchronized, lock和volatile区别(可见性、原子性、有序性)
属性 Synchronized lock volatile 解释 可见性 ✔ ✔ ✔ 变量被操作之后,能够快速写入内存,并提醒其他线程重读,加锁是通过一个一个执行保证了可见性 原子性 ✔ ✔ × 做的过程中,不要有相关的来打扰,不相关的我们也不关心,加锁是通过一个一个执行保证了流程不会被相关的打扰 有序性 ✔ ✔ ✔ 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性 synchronized与lock区别
类别 synchronized Lock 存在层次 Java的关键字,在jvm层面上 是一个接口类 锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁 锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待 锁状态 无法判断 可以判断 锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可) 性能 少量同步 大量同步 synchronized与volatile区别
类别 volatile synchronized 解决问题 变量在多线程之间的可见性 多个线程之间访问共享资源的同步性 可修饰 变量 方法、代码块 多线程下 不会阻塞 会阻塞 能否保证原子性 否 能,将私有内存和公有内存中的数据做同步 -
锁类型 描述 可重入锁 Synchronized和ReentrantLock都属于可重入锁,即某对象中有两个带有Synchronized或ReentrantLock的方法a和方法b,当一个线程获取到该对象锁后,在同步方法a里面可以调用同步方法b。表明了锁的分配机制:基于线程的分配而不是基于方法调用的分配。 中断锁 即可以响应中断的锁(线程a在执行带锁的代码片段,线程b在等待线程a执行完释放锁后去获取锁执行,但此时线程等了一段时间不想等了,线程b可以释放锁去做其它的事情),Synchronized不可中断,ReentrantLock可中断(调用方法:lockInterruptibly())。 公平锁 即尽量以请求锁的顺序来获取锁(比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁),Synchronized不公平,ReentrantLock公平(new ReentrantLock(true)即为公平锁)。 读写锁 对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写 线程状态
wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!
notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。
notifyAll():则是唤醒所有等待的线程。
这三个方法都属于Object。
线程状态 描述 新建 新建线程对象,并没有调用start()方法之前 就绪 调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦 运行 线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态 阻塞 线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态 死亡 线程执行结束
Synchronized
1、概述
2、作用域
- 对象实例:可以防止多个线程同时访问这个对象的synchronized方法,如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程就不能同时访问这个对象中任何一个synchronized方法。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
- 类:可以防止多个线程同时访问这个类所创建的对象中的synchronized方法。它可以对这个类创建的所有对象实例起作用;
3、代码
synchronized修饰方法
import java.util.stream.IntStream;
/**
* 当synchronized修饰方法时,
* 下面模拟多线程下面共用一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
* 当多线程下每个线程创建一个对象实例,不阻塞等待,模拟的多线程皆可以访问该对象下的所有带synchronized关键字的方法;
*/
public class SynchronizedUseMethod {
public synchronized void toDoFirst(int i) throws InterruptedException {
System.err.println(String.format("TODO first :[%s]",i));
Thread.sleep(3000);
System.out.println(String.format("TODO first :[%s] finish",i));
}
public synchronized void toDOSecond(int i) throws InterruptedException {
System.err.println(String.format("TODO second :[%s]",i));
Thread.sleep(3000);
System.out.println(String.format("TODO second :[%s] finish",i));
}
public static void test1(){
SynchronizedUseMethod synchronizedUse = new SynchronizedUseMethod();
IntStream.range(0,10).forEach(i->{
if (i%2==0) {
new Thread(()-> {
try {
synchronizedUse.toDoFirst(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}else{
new Thread(()-> {
try {
synchronizedUse.toDOSecond(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
});
}
public static void test2(){
IntStream.range(0,10).forEach(i->{
if (i%2==0) {
new Thread(()-> {
try {
SynchronizedUseMethod synchronizedUse1 = new SynchronizedUseMethod();
synchronizedUse1.toDoFirst(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}else{
new Thread(()-> {
try {
SynchronizedUseMethod synchronizedUse2 = new SynchronizedUseMethod();
synchronizedUse2.toDOSecond(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
});
}
public static void main(String[] args) {
test2();
}
}
synchronized修饰代码块时,括号内为this,当前对象时,同上
import java.util.stream.IntStream;
/**
* 当synchronized修饰代码块时,括号内为this,当前对象时,
* 下面模拟多线程下面共用一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
* 当多线程下每个线程创建一个对象实例,不阻塞等待,模拟的多线程皆可以访问该对象下的所有带synchronized关键字代码块的方法;
*/
public class SynchronizedUseCodeBlock {
public void toDoFirst(int i) throws InterruptedException {
synchronized (this){
System.err.println(String.format("TODO first :[%s]",i));
Thread.sleep(3000);
System.out.println(String.format("TODO first :[%s] finish",i));
}
}
public void toDOSecond(int i) throws InterruptedException {
synchronized(this){
System.err.println(String.format("TODO second :[%s]",i));
Thread.sleep(3000);
System.out.println(String.format("TODO second :[%s] finish",i));
}
}
public static void test1(){
SynchronizedUseCodeBlock synchronizedUse = new SynchronizedUseCodeBlock();
IntStream.range(0,10).forEach(i->{
if (i%2==0) {
new Thread(()-> {
try {
synchronizedUse.toDoFirst(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}else{
new Thread(()-> {
try {
synchronizedUse.toDOSecond(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
});
}
public static void test2(){
IntStream.range(0,10).forEach(i->{
if (i%2==0) {
new Thread(()-> {
try {
SynchronizedUseCodeBlock synchronizedUse1 = new SynchronizedUseCodeBlock();
synchronizedUse1.toDoFirst(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}else{
new Thread(()-> {
try {
SynchronizedUseCodeBlock synchronizedUse2 = new SynchronizedUseCodeBlock();
synchronizedUse2.toDOSecond(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
});
}
public static void main(String[] args) {
test1();
}
}
当synchronized修饰代码块时,括号内为类模板时
import java.util.stream.IntStream;
/**
* https://blog.csdn.net/qq_28082757/article/details/91542030
* 当synchronized修饰代码块时,括号内为类模板时,
* 下面模拟多线程下面共用一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
* 当多线程下每个线程创建一个对象实例,只要当一个线程获取到锁的时候,其它线程只能阻塞等待,直到线程释放锁,下一个线程获取锁,再进行下一步;
*/
public class SynchronizedUseClass {
public void toDoFirst(int i) throws InterruptedException {
synchronized (SynchronizedUseClass.class){
System.err.println(String.format("TODO first :[%s]",i));
Thread.sleep(3000);
System.out.println(String.format("TODO first :[%s] finish",i));
}
}
public void toDOSecond(int i) throws InterruptedException {
synchronized(SynchronizedUseClass.class){
System.err.println(String.format("TODO second :[%s]",i));
Thread.sleep(3000);
System.out.println(String.format("TODO second :[%s] finish",i));
}
}
public static void test1(){
SynchronizedUseClass synchronizedUse = new SynchronizedUseClass();
IntStream.range(0,10).forEach(i->{
if (i%2==0) {
new Thread(()-> {
try {
synchronizedUse.toDoFirst(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}else{
new Thread(()-> {
try {
synchronizedUse.toDOSecond(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
});
}
public static void test2(){
IntStream.range(0,10).forEach(i->{
if (i%2==0) {
new Thread(()-> {
try {
SynchronizedUseClass synchronizedUse1 = new SynchronizedUseClass();
synchronizedUse1.toDoFirst(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}else{
new Thread(()-> {
try {
SynchronizedUseClass synchronizedUse2 = new SynchronizedUseClass();
synchronizedUse2.toDOSecond(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
});
}
public static void main(String[] args) {
test1();
}
}
Lock
1、概述
- 1
- Lock接口主要方法:
- lock():获取锁,如果锁被暂用则一直等待;
- unlock():释放锁;
- tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true;
- tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间;
- lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事。
2、代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
/**
* Lock:可重入所锁,可判断,可公平
* https://blog.csdn.net/u012403290/article/details/64910926
* https://www.cnblogs.com/iyyy/p/7993788.html
*/
public class MyLock {
private final Lock lock = new ReentrantLock();//new ReentrantLock(true)为公平锁
/**
* 无论是线程共用对象实例还是各自用对象实例都会阻塞,直到上一个线程释放锁
* @param i
*/
public void useLock(int i) {
try {
lock.lock();
System.err.println(String.format("[%s]_useLock获取锁成功",i));
Thread.sleep(3000);
System.out.println(String.format("[%s]_useLock finish!!!",i));
}catch (InterruptedException e){
e.printStackTrace();
System.err.println(String.format("[%s]_useLock 释放锁失败!!!",i));
}finally {
lock.unlock();
System.err.println(String.format("[%s]_useLock 释放锁成功!!!",i));
}
}
//获取锁的时候锁被占用就返回false
public void useTryLock(int i) {
try {
if (lock.tryLock()) {
System.err.println(String.format("[%s]_useTryLock获取锁成功",i));
Thread.sleep(3000);
System.out.println(String.format("[%s]_useTryLock finish!!!",i));
}else{
System.err.println(String.format("当前锁被占用,无法获取"));
}
}catch (InterruptedException e){
e.printStackTrace();
System.err.println(String.format("[%s]_useTryLock 释放锁失败!!!",i));
}finally {
lock.unlock();
System.err.println(String.format("[%s]_useTryLock 释放锁成功!!!",i));
}
}
//设置的获取锁的等待时间结束后,锁还是被占用就返回false
public void useTryLockTime(int i) {
try {
if (lock.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.err.println(String.format("[%s]_useTryLock获取锁成功",i));
Thread.sleep(3000);
System.out.println(String.format("[%s]_useTryLock finish!!!",i));
}else{
System.err.println(String.format("当前锁被占用,无法获取"));
}
}catch (InterruptedException e){
e.printStackTrace();
System.err.println(String.format("[%s]_useTryLock 释放锁失败!!!",i));
}finally {
lock.unlock();
System.err.println(String.format("[%s]_useTryLock 释放锁成功!!!",i));
}
}
private static void test(){
MyLock myLock = new MyLock();
IntStream.range(0,12).forEach(i->{
int q = i % 3;
if (q==0) {
new Thread(()->myLock.useLock(i)).start();
}
if (q==1) {
new Thread(()->myLock.useTryLock(i)).start();
}
if (q==2) {
new Thread(()->myLock.useTryLockTime(i)).start();
}
});
}
public static void main(String[] args) {
test();
}
}
Volatile
1、概述
2、代码
下面代码不能保证原子性
扩展:关于volatile变量的可见性,经常被误解,认为一下描述成立:“volatile变量对所有线程时立即可见的,对volatile变量所有写操作都能立刻反应到其他线程之中,也就是说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分没有错,但是结论有问题。不要忘记了在Java里面的运算并非原子操作,这导致volatile变量的运算在并发的情况下一样是不安全的。
/**
* volatile变量自增运算测试
* 这段代码发起了30个线程,每个线程对num变量进行了1000次自增操作,如果这段代码能够正确并发的话,那么最后的输出结果应该是30000。
* 但是输出结果并不是我们期望的这样,而且基本上每次运行都会得到不同的结果。这是为什么呢?
* 问题就出在 “num++” 之中,这句代码看似只有一行,但是在Class文件中是由4条指令构成的
* cankao:https://blog.csdn.net/qq_28082757/article/details/91047241
*/
public class VolatileTest {
public static volatile int num = 0;
/**
* 自增1
*/
public static void increase(){
num++;
}
// 创建线程数量
private static final int THREADS_COUNT = 30;
public static void main(String[] args) {
// 先创建一个容量为 30 的 Thread 数组
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 对每一个 Thread 进行实例化,并使用匿名内部类实现 run 方法
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
// 每个线程调用1000 次自增方法
for (int j = 0; j < 1000; j++) {
increase();
}
}
});
threads[i].start();
}
//如果在IDEA里面进行调试的话,这里的判断条件改为2,因为IDEA会启动一个 Monitor Ctrl-Break 线程。
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(num);
}
}
通过java的concurrent包下提供了一些原子类AtomicInteger来保证原子性
/**
*AtomicInteger位于java.util.concurrent包下,使用了AtomicInteger代替了int后,程序输出了正确的结果,这一切都要归功于incrementAndGet方法的原子性
*/
public class AtomicTest {
public static AtomicInteger num = new AtomicInteger(0);
/**
* 自增1
*/
public static void increase(){
num.incrementAndGet();
}
// 创建线程数量
private static final int THREAD_COUNT = 30;
public static void main(String[] args) {
// 先创建一个容量为 30 的 Thread 数组
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
// 对每一个 Thread 进行实例化,并使用匿名内部类实现 run 方法
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
// 每个线程调用1000 次自增方法
for (int j = 0; j < 1000; j++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(num);
}
乐观锁和悲观锁
| 锁 | 描述 |
|---|---|
| 乐观锁 | 从名字就可以看出,乐观锁就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断在此期间别人有没有去更新这个数据,具体方法可以使用版本号机制和CAS算法。 |
| 悲观锁 | 乐观锁总是假设最好的情况,而悲观锁总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次拿数据都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系数据库就是悲观锁机制(行锁、表锁、读锁、写锁都是在操作之前先上锁)。java中的Synchronized和ReentrantLock独占锁就是悲观锁的实现。 |
1、乐观锁——版本号机制
- 描述:版本号机制一般是在数据表中加上一个数据库版本号version字段,它代表数据被修改的次数,当数据被修改时,version的值会加1。当一个线程要更新数据值时,在读取数据的同时也会读取version字段的值,在提交更新的时候,如果之前读取的version值与当前数据表中version字段值相等时才更新,同时将version值加1。否则重新读取最新数据和version值,尝试更新,直到更新成功。
- 举例:假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 );
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 );
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 ;
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。