多线程与高并发 synchronized 篇
进程 线程 协程/纤程(Quasur)
线程:一个程序里不同的执行路径
public static class T1 extends Thread{
@Override
public void run(){
System.out.println("Override Theme 中的 run 方法");
}
}
// 方法一
new MyThread().start();
// 方法二
new MyThread().start();
// 方法三
new Thread(()->{
Sout("Hello World!");
})
-
创建线程的两种方式:
- 创建一个类,继承Thread,重写方法
- 定义一个类,实现 Runnable 接口,然后重写 run 方法
-
启动:
//方法一:
new MyThread().start();
//方法二:
new Thread(new MyRun()).start();
//方法三:
new Thread(()->{ Sout("Hello World!");});
- 面试
- 问:启动线程的三种方式是?
- 答:
- 1、从
Thread
继承 - 2、实现
Runnable
接口 - 3、从线程池中启动
Executors.newCachedThrad
- 1、从
线程的基本方法
在T1中调用 T2.join();则执行到 join 之后,T1进入等待模式,先执行完T2之后,再返回执行T1。
// sleep 睡眠
Thread.sleep(500); // 毫秒
// Yield 让出一下CPU 进入等待队列(如果没有等待的则继续执行)
// 使用场景,较少
Thread.yield();
// Join
Thread T1 = new Thread(()->{
T1.join();
})
Thread T2 = new Thread(()->{
Sout("T2");
})
- 其他方法
// 暴力结束线程(不建议使用)
.stop()
// 唤起线程
.intereptor( )
// 获取线程状态。
.getState()
线程的锁
- 上锁
private int count = 10;
private Object o = new Object();
public void m(){
synchronized(0){ //任何县城要执行下面的代码,则必须先拿到o
count--;
}
}
// synchronized(this) 等值于 synchronized(方法);
public class T{
private static int count = 10;
public synchronized static void m(){ // 等同于synchronized(T.class)
count --;
}
public static void mm(){
synchronized(T.class){
count--;
}
}
}
synchronized的特性
1、锁的是对象,不是进程 / 线程 ; 2、能不加synchronized()锁就不加,加锁之后效率极低;
-
可重入性
- 一个方法
m1
加锁,另一个方法m2
也加了锁(同一把锁),那么m1
是可以调用m2
的。
- 一个方法
-
异常的锁
- 程序中的锁的内容出现了异常,那么该锁将被释放。
synchronized 底层实现
synchronized(Object) ; 括号中一定要是Object对象,不能是String 或其他任何类型;
早期的锁:需要就去找操作系统申请、
发展后的:先乐观锁、后自旋锁、最后找系统实现(重量级锁 | 最浪费时间)
- 抱着“没有线程跟我争用”的心态去申请一个资源:此时是 偏向锁,只记录ID,不锁(默认没有第二个线程来访问)
- 如果有线程争用:升级为 自旋锁 循环10次(占用CPU)
- 10次之后要访问的资源还被锁着?升级为重量级锁 去操作系统申请锁(不占用 CPU)
自旋锁:在用户态解决问题,不经过内核态。
执行时间长的用系统锁,(加锁代码)执行时间特别短,线程较少,用自旋锁。
总结
- Lock( ) CAS使用自旋
- synchronized 是一种锁,其锁的目标是 对象 而不是线程 / 进程(用对象代替进程更易操作)
- 被锁的对象 必须是 Object 类型,不能是 String 或其他对象
- 锁申请资源的时候一定是 :乐观锁
- 锁第一次升级的时候一定是:自旋锁(自旋十次,耗CPU,不走内核)
- 锁第二次升级的时候一定是:重量级锁(内核态,耗内核、耗时,不耗CPU)
多线程与高并发 2 代码优化 及 volatile修饰 篇
volatile 指令介绍
偏向锁 > 循环锁 > 重量级锁
指令介绍
- volatile // 可变的,易变的
指令功能
保证线程可见性,禁止指令重排序。
保证线程可见性:一个类的值给两个类同时调用,里面的变量改变后无法轻易发现(线程之间不可见)。
volatile可以让一个线程发生改变之后,另一个线程可以马上知道。
// 原理:CPU的缓存一致性协议。
禁止指令重排序:CPU迸发执行指令,所以会对指令重新排序,加了volatile来保证重排序。
举例介绍 及 代码优化
- 饿汉式:(定义类的时候就实例化方法)
public class Mgr01{
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01(){};
public static Mgr01 getInstance(){return INSTANCE;}
public void m() {System.out.println("m");}
public static void main(String[] args){
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
- 懒汉式:什么时候调用方法什么时候初始化(类似于懒加载)
public class Lazy{
private Lazy(){}
//默认不会实例化,什么时候用什么时候new
private static Lazy lazy=null;
public static synchronized Lazy getInstance(){
if(lazy==null){
lazy=new Lazy();
}
return lazy;
}
}
饿汉式 | 懒汉式 | |
---|---|---|
安全 | √ | |
节省内存 | √ |
- 懒汉饿汉合并:
类的定义:
public class Mgr01{ private /*volatile*/ static Mgr0x INSTANCE; private Mgr0x(){}; public static Mgr01 getInstance(){ //以下所有代码写的都是这一个方法 } }
以下所有方法写的都是上面的
getInstance()
方法。以上方法没有加
volatile
,最后会写上。
- 直接判断null
// 先判断是否为空 然后再那啥: public static Mgr03 getInstance(){ if(INSTANCE == null){ try{ Thread.sleep(1); }catch(InterruotedException e){ e.printStackTrace(); } INSTANCE = new Mgr03(); } return INSTANCE; }
↑ ↑ ↑ ↑ ↑ 这是一种错误的书写方式,自己抿;
- 先锁再null
public static synchronized Mgr04 getInstance(){ if(INSTANCE == null){ try{ Thread.sleep(1); }catch(InterruotedException e){ e.printStackTrace(); } INSTANCE = new Mgr04(); } return INSTANCE; }
↑ ↑ ↑ ↑ ↑ 修改正确,但是违背了 能不加锁就不加锁 原则。
- 锁细化:
public static Mgr05 getInstance(){ if(INSTANCE == null){ synchronized (Mgr05.class){ try{ Thread.sleep(1); }catch(InterruotedException e){ e.printStackTrace(); } INSTANCE = new Mgr05(); } } return INSTANCE; }
↑ ↑ ↑ ↑ ↑ 这也是一种错误的书写方式(重复初始化);
- 双重检查:
public static Mgr05 getInstance(){ if(INSTANCE == null){ synchronized (Mgr05.class){ if(INSTANCE == null){ try{ Thread.sleep(1); }catch(InterruotedException e){ e.printStackTrace(); } INSTANCE = new Mgr05(); } } } return INSTANCE; }
↑ ↑ ↑ ↑ ↑ 修改正确.......而且锁不加载外面,效率增高~~
- 关于
volatile
(主要是指令重排序
)超高超高迸发的情况可能会发生:// new对象的三步 INSTANCE = new Mgr06(); 1. 申请内存并给初始值(int = 0,String = null;) 2. 修改值 3. 将值给对象
加
volatile
防止第二步第三步会颠倒;
- 一个求结果是 100000 的小程序
public class T{ volatile int count = 0; // 加上vilatile synchronized void m(){ // 加上 synchronized for(int i=0;i<10000;i++){count++;} } public static void main(String[] args){ T t = new T(); List<Thread> threads = new ArraysList<~>(); for(int i=0;i<10;i++){ threads.add(new Thread(t::m,"threads-"+i)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try{ o.join(); } catch(InterruptedException e){ e.printStackTrace(); } }); System.out.println(t.count); } }
只有加上了 synchronized & volatile 才能运行出正确结果,其中 synchronized 用来保证
原子性
。
锁优化场景
- 锁力度变小(争用不是很激烈的话)
如果有一群要争用的代码,那么可以将方法上的 synchronized 写到 count++ 上;
- 锁力度变大(争用很激烈很频繁的话)
假如一个方法里面 总共 20 行代码,加了19个锁,那不如直接用一个大的锁。
锁的对象被调用
public class = T{
Object o = new Object();// 错误修改点
synchronized(0){
sout("123");
}
public void zbc(){
T t = new T();
t.o = "a";
}
↑ ↑ ↑ 以上代码错误!以下为修改 ↓ ↓ ↓
final Object o = new Object();
有些类在创建的时候直接加了锁
Atomic 开头的 (
AtomicInteger count = new AtomicInteger( );
// 让count进行原子性加减)
CAS ( Compare And Set ) 无锁优化 乐观锁
在请求的时候就乐观的认为 代码里的值就是我的期望值
cas (V ,Expected,NewValue){
if (V == Expected){
V = NewValue;
}else{
tryAgain or fail;
}
}
↑ ↑ ↑ 以上是在CPU 原语上的支持,不能被打断。
ABA
问题(与前女友复合之后,其实她已经经历了n个
男人;)
有个对象 object == 1;想使用cas
把它变成2:
cas(object,1,2);//没有线程进行操作,可以进行更改
如果在更改的时候有一个线程给 object 改成了2,然后又改成了 1 ;在基础类型(如:int)没有影响,但是 Object
对象有影响;
解决方法:做 cas
的时候加个版本号:version
解决方法:使用 AutomicStampedReference ( unsafe 什么时候调用什么时候返回这个值 )
思考
- 什么是 volatile ? 它有什么用?
- 什么是 synchronized ? 什么是 CAS ?两者有什么区别?分别在什么场景下使用?
- 什么时候要对锁进行细化?什么 时候进行泛化?
- 什么是 ABA 问题?有什么影响?怎么解决?
各式锁的实际应用
公平锁?不公平锁?乐观锁?悲观锁?自旋锁?重量级锁?读写锁?
乐观锁 cas
(要改的对象,期望的值,要给的值)无锁操作,其实是一个乐观锁......cas本身可以看成是一个锁;
- automic : 一种使用 cas 实现的原子性操作(上篇中提过)
原子操作的简单方法:
函数 | 效果 | 备注 |
---|---|---|
AtomicInteger a = new AtomicInteger(0); | int a = 0; | 创建对象a并且赋初值为0; |
a.incrementAndGet( ); | i++; | 对原值+1后返回; |
a.getAndIncrement( ); | ++i; | 对原值返回后+1; |
a.addAndGet(i); | a+=i; | 返回a+i; |
a.getAndAdd(i); | a+=i; | 返回原值之后给a+i; |
在线程很多的情况下:LongAdder(分段锁:在线程多的时候有优势) > Atomic > synchronized。
Synchronized 的可重入性:
//可重入: synchronized void m1(){ for(int i = 1;i<10;i++){ try{ TimeUtil.SECONDS.sleep(1);// 睡一秒 }catch(InterruptedException e){ e.printStackTrace(); } sout(i); } } synchronized void m2(){sout("m2...");} public static void main(String[] args){ T01_ReentrantLock1 r1 = new T01_ReentrantLock1(); new Thread(r1::m1).start(); try{ TimeUtil.SECONDS.sleep(1);// 睡一秒 }catch(InterruptedException e){ e.printStackTrace(); } new Thread(r1::m2).start(); }
输出结果:0 1 23 4 5 6 7 8 9 m2...
代码修改:synchronized
//可重入: synchronized void m1(){ for(int i = 1;i<10;i++){ try{ TimeUtil.SECONDS.sleep(1);// 睡一秒 }catch(InterruptedException e){ e.printStackTrace(); } sout(i); if(i == 2){ new Thread(r1::m2).start(); } } } synchronized void m2(){sout("m2...");} public static void main(String[] args){ T01_ReentrantLock1 r1 = new T01_ReentrantLock1(); new Thread(r1::m1).start(); try{ TimeUtil.SECONDS.sleep(1);// 睡一秒 }catch(InterruptedException e){ e.printStackTrace(); } }
输出结果:0 1 2 m2 ... 3 4 5 6 7 8 9
lock()
:替代 synchronized 的方法;
Lock lock = new ReentrantLock();
-
特点:
- 需要手动上锁
lock.lock( );
- 需要手动解锁
lock.unlock( )
; - 防止进程出错而导致死锁,需要
try{ …… }catch( ){ …… }
- 需要手动上锁
-
优点:
-
可以使用
tryLock()
尝试上锁; -
当
synchronized
遇到锁之后只能等待,而tryLock()
可以自定义等待时间; -
locked = lock.tryLock(SECONDS(时间长度),TimeUtil.SECONDS(时间格式:秒));
-
-
常用方法:
方法 | 参数 | 用法 |
---|---|---|
.lock( ); | null | 锁定 |
.unlock( ); | null | 释放 |
.tryLock(n,TimeUtil.SECONDS); | 时间长度 时间单位 |
等待参数 时间过程中:如果当前进程释放了,则锁定; 不释放则不锁定; |
.lockInterruptibly( ); | null; | 可以相应被打断的锁; |
.interrupt( ); | Null; | 打断这个锁; |
公平锁
ReetrantLock lock = new ReentrantLock( true );
- 概念:
- 当执行队列中有线程正在排队的时候:
- 公平锁:继续等待,排队执行;
- 不公平锁:不等待,直接抢,有可能抢到第一个执行;
- 当执行队列中有线程正在排队的时候:
- 创建方式:
- 在创建锁的时候加个
true
创建出来的就是公平锁;
- 在创建锁的时候加个
public class T05_ReentrantLock extends Thread(){
private stratic ReentrantLock lock = new ReentrantLock(true);
public void run(){
for(int i = 0;i<100;i++){
lock.lock();
try{
Sout(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
}
一个倒计时的门栓 CountDownLatch
CountDownLatch latch = CountDownLatch( threads.length );
//创建一个length长度的门栓
.await()
阻塞
原join()
当前线程结束自动往前走
.countDown()
原子性--
栅栏工具 CyclicBarrier
循环栅栏工具
// 一个参数:不到20的时候,等待,到了20个,这20个发车,再来的继续等待
CyclicBarrier barrier = new CyclicBarrier(20);
// 两个参数:
CyclicBarrier barrier = new CyclicBarrier(20,run);
run(){ Sout("满员,发车!"); }
//lambdo 表达式
CyclicBarrier barrier = new CyclicBarrier(20,()->Sout("满员,发车!"));
同步进行的 Phaser
按照不同的阶段对线程进行划分。
-
使用场景:
- 遗传算法
- 现实生活一步一步执行的场景(如:婚礼)
- 像是一个一个栅栏一样
-
使用方法:
-
自定义一个类,继承
Phaser
类;static class MarrigePhaser extends Phaser
-
重写
onAdvance
方法;(栅栏被推倒的时候自动调用)protected boolean onAdvance(int phase,int registeredParties)
-
-
方法:
phaser.arriveAndAwaitAdvance(); //执行结束,开始等待; phaser.arriveAndDeregister(); //执行结束,不进入下一阶段;
读写锁
程序中的读写锁(一种排他锁、共享锁)
-
概念
- 当
A
进程在读取ABCD
的时候,B
进程也来读取ABCD
,同时发现A
进程在读取,则读取成功; - 当
A
进程在读取ABCD
的时候,B
进程来修改ABCD
,同时发现A
进程在读取,若此时更改ABCD
的内容,则A
进程读取会出问题,所以修改失败; - 总结:两个都是读取的进程可以同时进行,当有 读 进程在进行时,无法进行 写 进程,写同理;
- 当
-
作用
- 避免 / 减少 脏数据
static ReadWriteLoak readWriteLock = new ReentrantReadWriteLock();
//在 ReentrantReadWriteLock 中 分出一个 `readLock`一个`writeLock`
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
Sout("read over!");
// 模拟读取过程
}catch(InterruptedException e){
e.peintStackTrace();
}finally{
lock.unlock();
}
}
public static void write(Lock lock,int a){
try{
lock.lock();
Thread.sleep(1000);
Sout("write "+ a +"over!");
// 模拟读取过程
}catch(InterruptedException e){
e.peintStackTrace();
}finally{
lock.unlock();
}
}
public static void main(String[] args){
Runnable readR = ()->read(lock);
//Runnable readR = ()->read(readLock);
Runnable write = ()->write(lock,new Random().nextInt());
for (int i=0;i<18;i++)new Thread(readR ).start();
for (int i=0;i<2 ;i++)new Thread(writeR).start();
}
}
// 如果使用 ReentrantLock
的话,以上代码在执行读
的时候也需要等待一秒;
// 解决方法:将Main
方法中的读
锁换成`Runnable readR = ()-> read(readLock);
Semaphore 一个有意思的线程池
Semaphore s = new Semaphore(x);
x是几则这个 < 线程池 > 就 允许几个线程 同时执行。
public static void main(String[] args){
Semaphore s = new Semaphore(1);
//括号中数字为x时,允许x个线程同时执行
// T1 Running
new Thread(()->{
try{
s.acquire();
// 进来一个进程 1 变成 0 ,别的线程不能执行
Sout("T1 Running");
Thread.sleep(200);
Sout("T1 Running");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
s.release();
// 离开一个进程 0 变成 1 ,别的线程可以执行
}
});
// T2 Running
new Thread(()->{
try{
s.acquire();
// 进来一个进程 1 变成 0 ,别的线程不能执行
Sout("T2 Running");
Thread.sleep(200);
Sout("T2 Running");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
s.release();
// 离开一个进程 0 变成 1 ,别的线程可以执行
}
});
}
如果x==1
则运行结果是T1 T1 T2 T2
,否则可能是T1 T2 T1 T2
Exchanger 用于 < ! 两个 ! > 线程交换数据的方法
使用场景:双人游戏中两人交换装备!执行一次就失效,可以循环等待下一次;
public static void main(String[] args){
// T1
new Thread(()->{
String s = "T1";
try{
s = sxchanger.exchange(s);
}cathc(InterruptedException e){
e.printStackTrace();
}
Sout(Thread.currentThread().getName()+""+s);
},"t1").start();
// T2
new Thread(()->{
String s = "T2";
try{
s = sxchanger.exchange(s);
}cathc(InterruptedException e){
e.printStackTrace();
}
Sout(Thread.currentThread().getName()+""+s);
},"t2").start();
}
线程中有两个变量,分别是 s 和 s (局部变量),两个线程同时执行,最后交换 T1 与 T2 的值;
分布式锁
只是某个类型的锁,将来补充概念。
总结 :
- 无论何种情况,优先考虑使用
synchronized
- 什么情况下使用lock()?它与 synchronized()相比有什么优点?
- 为什么使用读写锁?读写锁是怎么实现的?
- 随机列举一些跟锁一起使用的方法~~
- 把标题下的各种锁的使用场景和实现方式全都想一遍~