1.线程不安全的原因:
(下面这段代码:)
class Counter{ public int count = 0; public void increase(){ count++; } } public class Demo1 { private static Counter counter = new Counter(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread thread2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Counter:"+ counter.count); } }
- 在写多线程代码的时候,就需要考虑到在任意一种调度的情况下,都是能够运行出正确的结果的!
- 抢占式执行,线程不安全的重要原因。多个线程的调度执行过程是随机的,内核上也是这样实现的,没有办法修改!
- 多个线程同时修改一个变量,必须要满足多个线程、同时和修改这三个操作就会造成线程的不安全。有的时候可以调整代码从这里来下手解决线程安全问题,但是普适性不高。
- 线程操作不是原子的,原子表示不可分割的最小单位。解决线程安全最常见的方法就是把多个操作通过特殊的手段打包成一个原子操作。count++这样的操作本质上是3个CPU指令,第一是把内存的数据读取到CPU寄存器上;第二是把CPU的寄存器中的值进行+1;第三是把寄存器中的值写回到内存中。而CPU执行指令都是以一个指令为单位进行执行,一个指令就相当于CPU上的最小单位,一个指令如果执行一半就不会被调度走。
- 内存可见性问题,在JVM优化的背景下引入的bug。
- 指令重排序,在JVM优化的背景下引入的bug。
2.如何让代码的线程安全:
- 想办法把多个操作变成一个原子操作,怎么让count++变成原子?加锁!在count++之前先加锁,在count++之后再解锁。加锁和解锁之间只有当前调度的线程才可以修改,别的线程只能阻塞等待BLOCKED状态。
- 举个例子:俩个男生追同一个女生,A追到了,B就要阻塞等待直到女生分手,此时就发生了锁竞争。这里的A和B就是俩个线程,锁对象就是女生。再举个例子:还是俩个男生追不同的女生,A追到了女生1,A并不影响B追女生2,这种A和B的行动是互不干扰的,不存在锁竞争。
- 如何给对象加锁呢,这里用到关键字synchronized,当进入方法的时候加锁,方法执行完毕解锁。锁具有独占特性,如果当前锁没人来加,加锁操作就会成功;如果当前锁已经被人加上了,加锁操作就会阻塞等待。
- 本来线程调度就是随机的过程,一旦俩个组的load、add、save交织在一起就会产生线程安全问题。使用了锁之后,这俩组就能够串行执行了,一个执行完才会轮到下一个。这样就避免了线程不安全的问题。这个加锁的操作把并发变成了串行,会减慢代码的执行效率!
- 加完锁不是说CPU一口气就执行完了,中间也是有可能会有调度切换,即使t1切换走了,t2仍然是BLOCKED状态无法再在CPU上运行。线程调度出CPU,但是锁没释放,还是不能运行。
- 线程安全不是加了锁就一定安全,而是通过加锁让并发修改同一个变量=>串行修改同一个变量,在才会安全。如果加锁的方式不对那么不一定能解决线程安全问题。
- 如果只给一个线程加锁就不会涉及到锁竞争也就不会由并发修改=>串行修改。A加了锁,B就乖乖的阻塞等待,遵守规则才是安全的!
- 锁的代码越多叫做锁的粒度越大/越粗,锁的代码越少叫做锁的粒度越小/越细。
- synchronized的其他用法:synchronized的其他用法: 1.修饰方法。则锁对象就是this!
public synchronized void increase(){ count++; }
- 2.修饰代码块,可以把进行加锁的逻辑放到synchronized代码块中,也可以起到加锁作用。 如果一个方法中,有些代码需要加锁,有些不需要就可以使用这种修饰代码块的方式 格式synchronized(),括号里面填写的东西是要针对哪个对象加锁(被用来锁的对象称为锁对象) 写法1:
synchronized(this){ //大部分情况下无脑写this没问题,具体看使用的常场景 } 写法2: public Object locker = new Object(); public void int increase(){ synchronized(locker){ count++; } } 写法3: public static Locker locker = new Locker(); public void int increase(){ synchronized(locker){ count++; } }
- 在Java中任意对象都可以在synchronized里面作为锁对象。写多线程代码的时候,不关心这个锁对象究竟是谁是哪种形态,只是关心俩个线程是否是锁同一个对象,只有锁同一个对象才有竞争,锁不同的对象就没有竞争。
- 锁对象只是用来控制线程之间的互斥的,是针对同一个对象加锁就会出现互斥,针对不同对象加锁就不会互斥。
3.synchronized的使用:
无论锁对象是什么形态是什么类型,核心原则都是俩个线程争一个锁对象就有竞争,不同锁对象就没竞争。有锁竞争的目的是为了保证线程安全。
方法1.synchronized里面写的锁对象是this,谁调用了increase,就是针对谁加锁。下面这俩个线程都是针对counter对象进行加锁,因为是在针对同一个对象加锁所以这俩个线程执行的时候会出现互斥的情况。
class Counter{ public int count; public void increase(){ synchronized (this){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
方法2.线程t1里面的increase里的this代表counter,此时是针对counter加锁。线程t2里面的increase里的this代表counter2,此时是针对counter2加锁。这俩线程对不同的对象加锁,这里就不会出现锁竞争就不会出现阻塞等待问题。写法2不涉及线程安全问题!
class Counter{ public int count; public void increase(){ synchronized (this){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static Counter counter2 = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter2.increase(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
方法3.这个代码中,counter对象是同一个,对应的counter里面的locker就是同一个对象,此时仍然是俩个线程针对同一个对象加锁,会存在锁竞争。这个写法和写法1从线程安全的角度来看没有区别!
class Counter{ public int count; public Object locker = new Object(); public void increase(){ synchronized (locker){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
方法4.这种写法的locker是不同的对象,也就意味着这里是针对俩个不同对象加锁就不涉及锁竞争。
class Counter{ public int count; public Object locker = new Object(); public void increase(){ synchronized (locker){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static Counter counter2 = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter2.increase(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
方法5.这种写法的locker是一个静态成员,静态成员是唯一的。虽然创建了counter和counter2这俩个实例但是这俩的locker其实是同一个locker,因此这里也就会发生锁竞争。
class Counter{ public int count; static public Object locker = new Object(); public void increase(){ synchronized (locker){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static Counter counter2 = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter2.increase(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
方法6.线程t1对locker加锁,线程t2对counter加锁,此时俩线程是针对不同对象加锁不会产生锁竞争。
class Counter{ public int count; static public Object locker = new Object(); public void increase(){ synchronized (locker){ count++; } } public void increase2(){ synchronized (this){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase2(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
方法7.类对象Counter.class在JVM进程中只有一个,如果多个线程针对类对象加锁,势必就会锁竞争。
class Counter{ public int count; public void increase(){ //这里的锁对象变成了个类对象,类对象在JVM中只有一个 synchronized (Counter.class){ count++; } } } public class Demo3 { public static Counter counter = new Counter(); public static Counter counter2 = new Counter(); public static void main(String[] args) { Thread t1 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()-> { for (int i = 0; i < 50000; i++) { counter2.increase(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("counter="+counter.count); } }
4.synchronized的特性:
互斥、刷新内存、可重入。
(下面重点解释可重入):
- 一个线程连续针对一把锁,加锁俩次,就可能造成死锁。在第一次加锁,可以加锁成功;第二次加锁就会加锁失败,就会在第二次加锁这里阻塞等待,等到第一把锁被解锁,第二次加锁才能成功,第一把锁解锁则要求执行完synchronized代码块,也就是要求第二把锁能加锁成功!这里就形成了矛盾!
- 针对上面的情况,不会产生死锁的话,这样的锁就叫做“可重入锁”;会产生死锁,这个锁就叫做“不可重入锁”。synchronized是可重入的。
- 如何解决这种问题,避免死锁呢,线程t第一次尝试对this来加锁,this这个锁里面就记录了是线程t加的锁;第二次进行加锁的时候,锁看见还是线程t就直接通过了,没有任何负面影响,不会阻塞等待!举个例子:向女生表白如果妹妹接受了(加锁成功),拒绝你(加锁失败);当你俩在一起的时候,你再说一些甜言蜜语时,妹妹肯定也会接受。这就叫不会阻塞等待!
- 如何解决这种问题,避免死锁呢,引入一个计数器,每次加锁计数器就++,每次解锁计数器就--,如果计数器为0此时的加锁操作才是真加锁,如果计数器为0此时的解锁操作才是真解锁。
- 可重入锁的实现要点:第一是要让锁持有线程对象,记录是谁加了锁;第二是维护一个计数器,用来衡量什么时候是真加锁,什么时候是真解锁,什么时候是直接放行。
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹