-2022.3.27 -BDY
文章目录猛猪猪语录:争取早点学完JUC,太恶心了
Java并发二(线程安全)前言一、线程安全问题
1. 生动形象的故事2.线程问题原因**3.问题进一步探究****4.synchronized(对象锁) 解决方案** 二、线程八锁三、变量的线程安全分析(重点难点)
1、 成员变量和静态变量的线程安全分析 (重要)2、 局部变量线程安全分析 (重要)3、线程安全的情况 (重要)4、线程不安全的情况5、思考 private 或 final的重要性 (重要)6、常见线程安全类7、线程安全示例分析在这里插入图片描述 总结
前言
承接上文
一、线程安全问题 1. 生动形象的故事
public class Test {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 1; i < 5000; i++){
count++;
}
});
Thread t2 =new Thread(()->{
for (int i = 1; i < 5000; i++){
count--;
}
});
t1.start();
t2.start();
t1.join(); // 主线程等待t1线程执行完
t2.join(); // 主线程等待t2线程执行完
// main线程只有等待t1, t2线程都执行完之后, 才能打印count, 否则main线程不会等待t1,t2
// 直接就打印count的值为0
log.debug("count的值是{}",count);
}
}
// 打印: 并不是我们期望的0值, 为什么呢? 看下文分析
09:42:42.921 guizy.ThreadLocalDemo [main] - count的值是511
原因:
java静态变量的自增自减不是原子操作
(想想什么是静态变量,什么是原子操作)
(想想多线程并发的情况,抢夺cpu)
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
可以看到count++ 和 count-- 操作实际都是需要这个4个指令完成的
过程示例图:
临界区
一个程序运行多线程本身是没有问题的
问题出现在多个线程共享资源(临界资源)的时候
多个线程同时对共享资源进行读操作本身也没有问题 - 对读操作没问题
问题出现在对对共享资源同时进行读写操作时就有问题了 - 同时读写操作有问题
先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区; 共享资源也成为临界资源
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件
多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件
(所以出现问题的原因就是临界资源被多个线程调用,)
4.synchronized(对象锁) 解决方案
为了避免临界区中的竞态条件发生,由多种手段可以达到
阻塞式解决方案: synchronized , Lock (ReentrantLock)
非阻塞式解决方案: 原子变量 (CAS)
采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
1.语法
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
// 对临界资源(共享资源的操作) 进行 加锁
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
09:56:24.210 guizy.ThreadLocalDemo [main] - count的值是0
2.原理:
synchronized实际上利用对象锁保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断
思考:
如果把synchronized(obj)放在for循环的外面, 如何理解?
(for循环也是一个原子操作, 表现出原子性)
如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎么运行?
(因为t1, t2拿到不是同一把对象锁, 所以他们仍然会发现安全问题 – 必须要是同一把对象锁)
如果t1 synchronized(obj) 而 t2 没有加会怎么样 ?
(因为t2没有加锁, 所以t2, 不需要获取t1的锁, 直接就可以执行下面的代码, 仍然会出现安全问题)
3.synchronized 加在方法上
加在实例方法上(锁对象就是对象实例)
public class Demo {
//在方法上加上synchronized关键字
public synchronized void test() {
}
//等价于
public void test() {
synchronized(this) {
}
}
}
加在静态方法上(锁对象就是当前类的Class实例)
public class Demo {
//在静态方法上加上synchronized关键字
public synchronized static void test() {
}
//等价于
public void test() {
synchronized(Demo.class) {
}
}
}
二、线程八锁
其实就是考察synchronized 锁住的是哪个对象, 如果锁住的是同一对象, 就不会出现线程安全问题
1.、锁住同一个对象都是this(e1对象),结果为:1,2或者2,1
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象就是this, 也就是e1
public synchronized void a() {
log.debug("1");
}
// public void a () {
// synchronized (this) {
// log.debug("1");
// }
// }
// 锁对象也是this, e1
public synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e1.b()).start();
}
}
2.锁住同一个对象都是this(e1对象),结果为:1s后1,2 || 2,1s后1
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象就是this, 也就是e1
public synchronized void a(){
Thread.sleep(1000);
log.debug("1");
}
// 锁对象也是this, e1
public synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e1.b()).start();
}
}
3.a,b锁住同一个对象都是this(e1对象),c没有上锁。结果为:3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象就是this, 也就是e1
public synchronized void a() throws InterruptedException {
Thread.sleep(1000);
log.debug("1");
}
// 锁对象也是this, e1
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
new Thread(() -> {
try {
e1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> e1.b()).start();
new Thread(() -> e1.c()).start();
}
}
4.a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥。结果为:2,1s后1
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象是e1
public synchronized void a() {
Thread.sleep(1000);
log.debug("1");
}
// 锁对象是e2
public synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
EightLockTest e2 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e2.b()).start();
}
}
5.a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象是EightLockTest.class类对象
public static synchronized void a() {
Thread.sleep(1000);
log.debug("1");
}
// 锁对象是e2
public synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e1.b()).start();
}
}
6.a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象是EightLockTest.class类对象
public static synchronized void a() {
Thread.sleep(1000);
log.debug("1");
}
// 锁对象是EightLockTest.class类对象
public static synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e1.b()).start();
}
}
7.a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象是EightLockTest.class类对象
public static synchronized void a() {
Thread.sleep(1000);
log.debug("1");
}
// 锁对象是this,e2对象
public synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
EightLockTest e2 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e2.b()).start();
}
}
8.a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
// 锁对象是EightLockTest.class类对象
public static synchronized void a() {
Thread.sleep(1000);
log.debug("1");
}
// 锁对象是EightLockTest.class类对象
public static synchronized void b() {
log.debug("2");
}
public static void main(String[] args) {
EightLockTest e1 = new EightLockTest();
EightLockTest e2 = new EightLockTest();
new Thread(() -> e1.a()).start();
new Thread(() -> e2.b()).start();
}
}
三、变量的线程安全分析(重点难点) 1、 成员变量和静态变量的线程安全分析 (重要)
想想什么是成员变量(类的变量)什么是静态变量(static)
2、 局部变量线程安全分析 (重要)如果变量没有在线程间共享,那么变量是安全的
如果变量在线程间共享
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
想想什么是局部变量,方法内的变量
3、线程安全的情况 (重要)局部变量【局部变量被初始化为基本数据类型】是安全的
但局部变量引用的对象则未必 (要看该对象是否被共享且被执行了读写操作)
如果该对象没有逃离方法的作用范围,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全
4、线程不安全的情况局部变量表是存在于栈帧中, 而虚拟机栈中又包括很多栈帧, 虚拟机栈是线程私有的;
局部变量【局部变量被初始化为基本数据类型】是安全的
局部变量引用的对象逃离方法的范围,那么要考虑线程安全问题的
示例:
public class Test15 {
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
for (int i =0;i<100;i++){
new Thread(()->{
unsafeTest.method1();
},"线程"+i).start();
}
}
}
class UnsafeTest{
ArrayList arrayList = new ArrayList<>();
public void method1(){
for (int i = 0; i < 100; i++) {
method2();
method3();
}
}
private void method2() {
arrayList.add("1");
}
private void method3() {
arrayList.remove(0);
}
}
Exception in thread "线程1" Exception in thread "线程2" java.lang.ArrayIndexOutOfBoundsException: -1
原因分析:
多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 进行上下文切换,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍等于0
解决方法:
1.可以将list修改成局部变量,局部变量存放在栈帧中, 栈帧又存放在虚拟机栈中, 虚拟机栈是作为线程私有的;
2.因为method1方法, 将arrayList传给method2,method3方法, 此时他们三个方法共享这同一个arrayList, 此时不会被其他线程访问到, 所以不会出现线程安全问题, 因为这三个方法使用的同一个线程。
3.在外部, 创建了100个线程, 每个线程都会调用method1方法, 然后都会再从新创建一个新的arrayList对象, 这个新对象再传递给method2,method3方法.
简而言之:让每个方法指向自己的对象
5、思考 private 或 final的重要性 (重要)如果改为public, 此时子类可以重写父类的方法, 在子类中开线程来操作list对象, 此时就会出现线程安全问题: 子类和父类共享了list对象
如果改为private, 子类就不能重写父类的私有方法, 也就不会出现线程安全问题; 所以所private修饰符是可以避免线程安全问题.
所以如果不想子类, 重写父类的方法的时候, 我们可以将父类中的方法设置为private, final修饰的方法, 此时子类就无法影响父类中的方法了!
private:私有的
final:
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类 JUC
重点:
1.多个线程调用它们同一个实例的某个方法时,是线程安全的 , 也可以理解为 它们的每个方法是原子的(被加上了synchronized)
Hashtable table = new Hashtable();
new Thread(()->{
// put方法增加了synchronized
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
2.多个方法的组合不是原子的,所以可能会出现线程安全问题
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
不可变类的线程安全
String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的, 都被final修饰, 不能被继承.
主要介绍线程的安全
为什么会出现问题——》原因,原理
出现问题的解决方法——》synchronized
以及变量是否会出现线程安全问题——》弄懂线程安全的类
有疑惑的地方:感觉对于线程安全 问题的判断还是不够,理解不够到位
对于处理线程安全问题的办法可以接着学习



