文章目录
并发关键字
synchronized
可以理解为汽车的自动挡,对应的Lock就是手动挡~
概念
即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心现成话上下文切换
使用
只有在静态方法的时候锁的是类对象
修饰目标 | 锁 |
---|---|
方法 | 当前实例对象 |
静态方法 | 类对象 |
代码块 | 当前实例对象 |
class对象 | 类对象 |
任意Object对象 | 任意示例对象 |
实例和讲解
实例
/**
* @author qijian
* @description
* @date 2021/4/12 - 10:09
*/
public class Synchronized {
//synchronized关键字可放于方法返回值前任意位置,本示例应当注意到sleep()不会释放对监视器的锁定
//实例方法
public synchronized void instanceMethod() {
for (int i = 0; i < 5; i++) {
System.out.println("instanceMethod");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//静态方法
public synchronized static void staticMethod() {
for (int i = 0; i < 5; i++) {
System.out.println("staticMethod");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void thisMethod() {
//this对象
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println("thisMethod");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void classMethod() {
//class对象
synchronized (Synchronized.class) {
for (int i = 0; i < 5; i++) {
System.out.println("classMethod");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void anyObject() {
//任意对象
synchronized ("anything") {
for (int i = 0; i < 5; i++) {
System.out.println("anyObject");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
讲解
1、普通方法和代码块中使用this是同一个监视器(锁),即某个具体调用该代码的对象
public static void main(String[] args) {
Synchronized syn = new Synchronized();
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
syn.thisMethod();
}
}.start();
new Thread() {
@Override
public void run() {
syn.instanceMethod();
}
}.start();
}
}
2、静态方法和代码块中使用该类的class对象是同一个监视器,任何该类的对象调用该段代码时都是在争夺同一个监视器的锁定
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Synchronized syn = new Synchronized();
new Thread() {
@Override
public void run() {
Synchronized.staticMethod();
}
}.start();
new Thread() {
@Override
public void run() {
syn.classMethod();
}
}.start();
}
}
特点
- 可重入性:当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果
- 自动释放:当代码段执行结束或出现异常后会自动释放对监视器的锁定
- 非公平锁:在等待获取锁的过程中不可被中断
- 互斥性:被 synchronized 修饰的方法同时只能由一个线程执行
锁升级
- Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,这几个状态会随着竞争情况逐渐升级
- 锁可以升级但不能降级,意味着 偏向锁 升级成 轻量级锁 后不能降级成 偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
概念
- HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否 存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
运作:
关闭偏向锁
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活:
-
如有必要可以使用JVM参数来关闭延迟:
-XX:BiasedLockingStartupDelay=0
-
如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁,那么程序默认会进入轻量级锁状态。:
-XX:UseBiasedLocking=false
轻量级锁/重量级锁
概念
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
8锁现象
深刻理解我们的锁:如何判断锁的是谁?
1、两个同步方法(先发短信后打电话)
public class dome01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendMs(); }).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
public synchronized void sendMs() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
原因:顺序执行下去
2、我们让 发短信 延迟4s(先发短信后打电话)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
public synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
原因:并不是顺序执行,而是synchronized 锁住的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待
3、加一个普通方法(先hello再发短信)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone.hello(); }).start();
}
}
class Phone {
public synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
原因:hello是一个普通方法,不受synchronized锁的影响,不用等待锁的释放
4、如果我们使用的是两个对象,一个调用发短信,一个调用打电话(先打电话后发短信)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone1.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone2.call(); }).start();
}
}
class Phone {
public synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
原因:两个对象两把锁,不会出现等待的情况,发短信睡了4s,所以先执行打电话
5、如果我们把synchronized的方法加上static变成静态方法
(1)我们先来使用一个对象调用两个方法!(先短信后电话)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
public static synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
(2)如果我们使用两个对象调用两个方法!(还是先短信后电话)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone1.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone2.call(); }).start();
}
}
class Phone {
public static synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法而已
6、如果我们使用一个静态同步方法、一个同步方法(先打电话后发短信)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
// Phone phone2 = new Phone();
new Thread(() -> {
try {
phone1.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone1.call(); }).start();
}
}
class Phone {
public static synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
原因:因为一个锁的是Class类的模板,一个锁的是对象的调用者。所以不存在等待,直接运行。
7、如果我们使用一个静态同步方法、一个同步方法,两个对象调用(先打电话后发短信)
public class dome01 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone1.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> { phone2.call(); }).start();
}
}
class Phone {
public static synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
原因:两把锁锁的不是同一个东西
小结
要看清 synchronized 锁的到底是什么,才能准确判断
lock
手动挡
概念
- 在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能
- 它提供了与synchronized关键字类似的同步功能。虽然它缺少了(通过synchronized块或者方法)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用
package com.marchsoft.juctest;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description:
*
* @author jiaoqianjin
* Date: 2020/8/10 22:05
**/
public class LockDemo {
public static void main(String[] args) {
final Ticket2 ticket = new Ticket2();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "C").start();
}
}
//lock三部曲
//1、 Lock lock=new ReentrantLock();
//2、 lock.lock() 加锁
//3、 finally=> 解锁:lock.unlock();
class Ticket2 {
private int number = 30;
// 创建锁
Lock lock = new ReentrantLock();
//卖票的方式
public synchronized void sale() {
lock.lock(); // 开启锁
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}finally {
lock.unlock(); // 关闭锁
}
}
}
方法
从上,可以看出 Synchronized 与 Lock 的区别:
-
Synchronized 内置的Java关键字,Lock是一个Java类;
-
Synchronized 无法判断获取锁的状态,Lock可以判断锁的状态;
-
Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁;
-
Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待;
-
Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
-
Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码。
缺点
- 使用比较复杂,这点之前提到了,需要手动加锁,解锁,而且还必须保证在异常状态下也要能够解锁。而synchronized的使用就简单多了
- 效率较低,synchronized关键字毕竟是jvm底层实现的,因此用了很多优化措施来优化速度(偏向锁、轻量锁等),而显示锁的效率相对低一些
ReadWriteLock
读写锁
不加锁
所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
int num = 6;
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
/**
* 方法未加锁,导致写的时候被插队
*/
class MyCache {
private volatile Map<String, String> map = new HashMap<>();
public void write(String key, String value) {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入ok");
}
public void read(String key) {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程写读取ok");
}
}
加锁
- 可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
- 但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache2 myCache = new MyCache2();
int num = 6;
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.write(String.valueOf(finalI), String.valueOf(finalI));
},String.valueOf(i)).start();
}
for (int i = 1; i <= num; i++) {
int finalI = i;
new Thread(() -> {
myCache.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
class MyCache2 {
private volatile Map<String, String> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(String key, String value) {
lock.writeLock().lock(); // 写锁
try {
System.out.println(Thread.currentThread().getName() + "线程开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "线程写入ok");
}finally {
lock.writeLock().unlock(); // 释放写锁
}
}
public void read(String key) {
lock.readLock().lock(); // 读锁
try {
System.out.println(Thread.currentThread().getName() + "线程开始读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "线程写读取ok");
}finally {
lock.readLock().unlock(); // 释放读锁
}
}
}
volatile
volatile是Java提供的一种轻量级的同步机制
- 相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错
- 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象
保证可见性
当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去
public class JMMDemo01 {
// 不加volatile:程序会死循环
// 加了volatile:是可以保证可见性的
private volatile static Integer number = 0;
public static void main(String[] args) {
//main线程
//子线程1
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//子线程2
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number=1;
System.out.println(number);
}
}
不保证原子性
原子性:线程A在执行任务的时候,不能被打扰/分割,要么同时成功,要么同时失败。
这个写会操作会导致其他线程中的volatile变量缓存无效(原子性:同时成功/失败)
public class VDemo02 {
private static volatile int number = 0;
public static void add(){
number++; //++ 不是一个原子性操作,是两个~3个操作
}
public static void main(String[] args) {
//理论上number 为 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
//main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+number);
}
}