共享问题
资源共享
- 由于线程和进程的关系,在上一篇并发基础中有写到,多个线程可共享一块内存区域,并且每个线程也都有自己独立的工作空间。
- 经典案例:从这里我们开始对自增自减进行迫害
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
- 上述代码的结果会是0吗?
- 分析:结果可能是正数、负数或者0。why? 因为java中的自增自减不是原子操作。
- 上字节码: i++
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
- 单线程下,结果肯定是0,但是多线程环境下,自增和自减的指令交错,就会出现问题,
- 比如t1线程刚获取静态变量i的值为1,t2线程立马完成减1操作并且赋值,t1线程操作的值是1,而实际上i已经变成了0
临界区
- 一个程序在运行的时候要考虑多线程对他的影响,也就是多个线程在同时访问一个共享资源的时候,如果读写指令发生交错,那就乱套了
- 一段代码内如果存在对 共享资源的多线程读写操作,称这段代码块为临界区
- 举个栗子
static int i=0;
static void add(){
//临界区
i++;
}
static void decre(){
//临界区
i--;
}
- 补充:自增和自减操作我们从字节码里可以看出,分为读和写组成的,并非原子性操作。
竞态条件
多个线程在临界区执行,由于代码的执行顺序问题而导致结果的不可见,无法预测,称之为发生竞态条件
synchronized
用阻塞的方式治疗
- synchronized作为java的一个关键字,它能够将一个方法或者一段代码块锁起来,确定同一时间内,只有一个线程在执行被锁住的代码。
- synchronized通过互斥的方式,能够解决上面的临界区问题,也是阻塞式的。
- 其他线程想来获得这个锁,那就需要进行等待(阻塞),确保拥有锁的线程能够安全的执行完这段被锁住的代码。
==互斥和同步都可以通过synchronized关键字实现,but二者是有区别滴
- 互斥是保证临界区的竞态条件发生,同一时刻只有一个线程执行临界区代码
- 同步是由于线程完成任务的先后、顺序不同,需要一个线程等待另一个线程完成到某个点
==
语法
synchronized (obj){
//代码块
}
//解决上面的问题
static int counter = 0;
static final Object obj=new Object()
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized(obj){
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized(obj){
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
方法上的synchronized
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
线程安全分析
成员变量和静态变量是否线程安全?
- 如果没有共享,则安全
- 如果被共享,根据他们的状态是否能改变,又分为
- 只有读操作,那没事了
- 如果有读写操作,则这段代码就是临界区,需要考虑线程安全问题
局部变量是否安全?
- 局部是线程安全的
- 但是局部变量引用的对象不一定
- 如果该对象没有逃离方法的作用范围,他是安全的
- 如果对象逃离方法的作用范围,那就要考虑线程安全问题
public static void test1() {
int i = 10;
i++;
}
每个线程在调用test1方法是,i变量都会创建一次,不存在共享的问题
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>(); //成员变量
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
//执行
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
分析:
- ThreadUnsafe中的list是成员变量,无论是哪个线程中method2引用的都是同一个list变量 共享!
- 这里执行的是调用method1方法,method1去调用的method2和3,这两个方法中的list是同一个。
- 如果method2还没有执行,list中还为空,那么执行了方法3就会报错。
解决:将list改为局部变量
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
方法修饰符带来的线程安全问题
子类进行方法覆盖
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
Monitor
java对象头
- 32位虚拟机为例
- 64位虚拟机为例
Monitor锁原理
- Monitor被译为监视器或管程
- 每个java对象都可以关联一个监视器,如果使用synchronized上锁后,该对象的Monitor中的Mark Word中就被设置为指向Monitor对象的指针
- 结构如下
- 注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
wait/notify
API
- wait:让当前对象锁的拥有者Onwer线程,进入到该对象头中的WaitSet等待,无限制等待
- notify:随机唤醒WaitSet中的一个线程
- notifyAll:唤醒所有WaitSet中的线程
wait/notify的正确姿势
- sleep和wait的区别,sleep是线程调用的方法,将该线程进行睡眠,而且期间不会释放该线程所持有的资源;wait是在调用synchronized锁之后,通过锁对象调用的方法,而且会释放锁资源
- 完成一个场景,小王和小李在厂里上班,小王做到一半想抽根华子,不抽不干活,等着人拿烟来;小李饿了,等着美团送外卖,没吃饭也不干活
@Slf4j
public class Test11 {
static boolean hasHuaZi=false;
static boolean hasWM=false; //外卖
static Object obj=new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
log.debug("开始干活...");
log.debug("来根华子好吗?秋梨膏");
log.debug("开始等人送华子");
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
log.debug("烟来了吗?");
if (hasHuaZi) {
log.debug("来劲了,开始干活,芜湖~");
break;
}else {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
},"小王").start();
new Thread(()->{
log.debug("开始干活...");
log.debug("没吃饭不干活");
log.debug("点外卖了,等~ing");
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
log.debug("外卖来了吗?");
if (hasWM) {
log.debug("吃饱了,开始干活,芜湖~");
break;
}else {
log.debug("没来再歇会儿");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
},"小李").start();
Thread.sleep(2000);
new Thread(()->{
synchronized (obj){
log.debug("烟来咯");
hasHuaZi=true;
obj.notifyAll();
}
},"送烟的").start();
Thread.sleep(2000);
new Thread(()->{
synchronized (obj){
log.debug("美团外卖");
hasWM=true;
obj.notifyAll();
}
},"美团").start();
}
}
park和unpark
可以说是加强版的wait/notify
基本使用
//这两个方法是LockSupport类中的方法
//暂停某个线程
LockSupport.park();
//恢复指定的线程 t1
LockSupport.unpark(t1);
先Park 再Unpark
@Slf4j
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("run....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
log.debug("unpark...");
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("run...");
LockSupport.park();
log.debug("unpark...");
}, "t2");
t1.start();
t2.start();
Thread.sleep(1000);
LockSupport.unpark(t1);
}
}
结果
先unpark再park
@Slf4j
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("run....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park");
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("unpark");
LockSupport.unpark(t1);
}
}
结果:我们先对t1进行了unpark,然后再进行park,结果发现t1依然unpark了
原理:
- 每个线程都有一个自己的Parker对象,由三部分组成: _counter , _cond 和 _mutex
- 设想一个线程是一个旅行者:Parker就是他的背包,_counter则是包里的干粮,0表示没有,1表示充足
- 调用park并不是直接就休息,而是检查干粮是否充足,如果没有了,则进行休息;如果充足则继续,不用停止
- 调用unpark,就好比补充干粮,如果此刻在休息,则就叫醒出发;如果还在运行,则会补充一份备用干粮,那么当他下次调用park的时候,仅仅是耗掉备用干粮,也就是说,并不会停下来,因为只是消耗了备用干粮,_counter还是1,干粮依旧充足
- _counter初始状态为0,当进行park检查时,发现为0,则暂停,获得**_mutex锁,进入_cond**等待
- 然后调用 unpark,现将**_counter**设置为1;
- 唤醒**_cond**条件变量中的线程
- 线程恢复运行
- _counter设值为0
以上是先park,再unpark
- 一波非常规操作,先unpark,将**_counter** 设置为1
- 然后调用park的时候,发现 _counter 为1,则不暂停线程
- 将**_counter** 设置为0
活跃性
死锁
当一个线程需要获取多把锁时,这时容易发生死锁
例如,t1先获取了A锁,然后要再去获取B锁;此时t1刚获取A锁的时候,t2获得了B锁,而t2又需要再去获得A锁。
定位死锁的工具 jconsole
活锁
两个线程相互改变对方的结束条件,最后谁也无法结束
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
饥饿
线程始终得不到CPU调度,也不能结束
ReentrantLock
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
可打断: lockInterruptibly
@Slf4j
public class Test12 {
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("run....");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
}
try {
log.debug("获得了锁");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}
锁超时:tryLock
立刻返回
@Slf4j
public class Test12 {
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("run....");
if (!lock.tryLock()){
log.debug("尝试获得锁失败。。。立即返回");
return;
}
try {
log.debug("获得锁成功");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
try {
Thread.sleep(1000);
} finally {
lock.unlock();
}
}
}
锁超时
@Slf4j
public class Test12 {
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("run....");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)){
log.debug("等待1秒后尝试获得锁失败。。。立即返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得锁成功");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
try {
Thread.sleep(2000);
} finally {
lock.unlock();
}
}
}
公平锁
条件变量
@Slf4j
public class Test12 {
static ReentrantLock lock=new ReentrantLock();
static Condition waitRoom1=lock.newCondition();
static Condition waitRoom2=lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("run....");
lock.lock();
try {
Thread.sleep(2000);
} catch (Exception e){
e.printStackTrace();
}
try {
log.debug("进入休息室1...waitRoom1");
waitRoom1.await();
log.debug("醒了...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}, "t1");
t1.start();
Thread.sleep(4000);
log.debug("叫醒他");
lock.lock();
try {
waitRoom1.signal();
} finally {
lock.unlock();
}
}
}