栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java并发二(线程安全)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java并发二(线程安全)

Java并发二(线程安全)

-2022.3.27 -BDY

猛猪猪语录:争取早点学完JUC,太恶心了

文章目录

Java并发二(线程安全)前言一、线程安全问题

1. 生动形象的故事2.线程问题原因**3.问题进一步探究****4.synchronized(对象锁) 解决方案** 二、线程八锁三、变量的线程安全分析(重点难点)

1、 成员变量和静态变量的线程安全分析 (重要)2、 局部变量线程安全分析 (重要)3、线程安全的情况 (重要)4、线程不安全的情况5、思考 private 或 final的重要性 (重要)6、常见线程安全类7、线程安全示例分析在这里插入图片描述 总结


前言

承接上文


一、线程安全问题 1. 生动形象的故事




2.线程问题原因
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个指令完成的

过程示例图:

3.问题进一步探究

临界区

一个程序运行多线程本身是没有问题的
问题出现在多个线程共享资源(临界资源)的时候
多个线程同时对共享资源进行读操作本身也没有问题 - 对读操作没问题
问题出现在对对共享资源同时进行读写操作时就有问题了 - 同时读写操作有问题
先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区; 共享资源也成为临界资源

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:

6、常见线程安全类

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修饰, 不能被继承.

7、线程安全示例分析 总结

主要介绍线程的安全
为什么会出现问题——》原因,原理
出现问题的解决方法——》synchronized
以及变量是否会出现线程安全问题——》弄懂线程安全的类
有疑惑的地方:感觉对于线程安全 问题的判断还是不够,理解不够到位
对于处理线程安全问题的办法可以接着学习

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/781883.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号