java并发编程笔记(三)--管程(一)

4.共享模型之管程

4.1.线程中的安全问题

举个例子:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

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。

宏观来讲:这是线程上下文切换引起的问题,在上一个线程操作还没结束时,就切换成了下个线程,导致之前线程的操作没有及时赋值,引起了多种结果。

要想更深刻的理解这个问题,我们还需要从字节码的角度来分析。

例如对于 i++而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

两个线程不断对主内存里的静态变量i进行操作:

QQ截图20220203180057

而时间片的时间不一定一定卡在恰好完成i++ 或者 i-- 操作时,所以会引起以下情况:

运行正确的情况:一个线程/恰好在每个时间片耗完时操作恰好完成

QQ截图20220203180325

出现负数的情况:

QQ截图20220203180437

线程二对i进行减法操作还没赋值,时间片给了线程一,线程一将i操作后赋值为 1,然后结束操作,到了线程二运行,完成最后一步,将i错误的赋值成了-1。

正数的情况:

QQ截图20220203180659

上面的解释反过来就是正数的情况。

但实际上,线程安全问题单论对共享资源的读操作,是不会出错的,但是一旦涉及到对共享资源的读写操作,就容易引起线程安全问题。而如果存在对共享资源的多线程读写操作,称这段代码块为临界区

4.1.1.临界区

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如上述例子的 i++ i–的代码块就是临界区:

static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++;
}

static void decrement() 
// 临界区
{ 
 counter--;
}

4.1.2.竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

上述情况就是发生了竞态条件。

4.2.synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

可以使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一

时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁

的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

4.2.1.synchronized的原理

synchronizd 的使用我们可以这样理解

首先 sychronized是锁,他锁住了临界区的代码,而锁包含的对象是钥匙

被sychronized 修饰的临界区都被锁在了 以room对象为钥匙的保险柜里,每个拿到该对象的线程相当于拿到了保险柜的钥匙,可以自由使用临界区里的代码,其他没有对象的线程,因为没有相应的钥匙,所以拿不到该代码。

只有当拥有钥匙的线程的临界区的代码执行完了(在保险柜里干完了自己的事情),才会把钥匙给下个先来的线程。

其他没钥匙的线程,到了执行它们临界区代码的时候,只能暂时在门外等当前有钥匙的线程执行结束。

从而保证了一个对象锁住的代码只能同时一个线程使用。

如图

QQ截图20220203181829

4.2.2.synchronized用法

  • 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
 临界区
}
  • 解决上述问题
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);
}

sychronized将线程一和线程二的两个临界区都锁上了,并且用room对象作为钥匙,保证了只有持有room对象的线程才能进行对应的自己的临界区操作,保证了线程安全。

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切

换所打断。

4.2.3.面向对象改进

刚才我们将临界区都写在了线程里,显得较为杂乱,这次我们试着写进定义了锁的类里。

class Room {
//我们把value比做上面的i    
 int value = 0;
    
  //i++方法  
 public void increment() {
 synchronized (this) {
 value++;
 }
 }
    
 //i--方法
 public void decrement() {
 synchronized (this) {
 value--;
 }
 }
//获取 i    
 public int get() {
 synchronized (this) {
 return value;
 }
 }
    
}

@Slf4j
public class Test1 {
 
 public static void main(String[] args) throws InterruptedException {
 Room room = new Room();
     
 Thread t1 = new Thread(() -> {
 for (int j = 0; j < 5000; j++) {
 room.increment();
  }
 }, "t1");
     
 Thread t2 = new Thread(() -> {
 for (int j = 0; j < 5000; j++) {
 room.decrement();
 }
 }, "t2");
     
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 log.debug("count: {}" , room.get());
 }
}

**4.2.4.**synchronized在方法上的简写

在非静态方法上加synchronized

class Test{
 public synchronized void test() {
 
 }
}

//等价于

class Test{
 public void test() {
 synchronized(this) {
 
 }
 }
}

在静态方法上加synchronized

class Test{
 public synchronized static void test() {
 }
}

//等价于
class Test{
 public static void test() {
 synchronized(Test.class) {
 }
 }
}

4.2.5.线程八锁问题

一些小练习,判断运行结果。

QQ截图20220203182956

QQ截图20220203183004

QQ截图20220203183010

QQ截图20220203183015

QQ截图20220203183024

QQ截图20220203183031

QQ截图20220203183038

4.3 变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全

  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

    共享且有读写操作不一定安全。

局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必

    • 如果该对象没有逃离方法的作用访问,它是线程安全的

    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量如果逃离作用域,线程不一定安全。

4.3.1.局部变量线程安全分析

例子如下:

public static void test1() {
 int i = 10;
 i++; 
}

字节码如下:

public static void test1();
descriptor: ()V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I

该局部变量是线程安全的。

局部变量的解释如下:

QQ截图20220203184948

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。故线程安全。

4.3.2.成员变量线程安全分析

举个例子:

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();
 
    }
}

无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量

如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748)

原理如下图,因为公用堆里的list对象,所以线程不安全。

QQ截图20220203185753

若将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);
 }
}

则线程一和线程二都会有一个list,问题得到解决。

QQ截图20220203190815

而即使是局部变量,如果类被继承,方法被修改,也难以保证线程安全。

class ThreadSafe {
 //public final void method1(int loopNumber) 就可以解决这个问题
 public  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 ThreadSafeSubClass extends ThreadSafe{
 @Override
 public void method3(ArrayList<String> list) {
 new Thread(() -> {
 list.remove(0);
 }).start();
 }
}

此时用final加在方法前就可以保证线程安全,从这点我们可以体会到final在线程安全里的重要闭包作用。

4.4.常见线程安全类

  • String
  • Integer
  • StringBuffffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();

new Thread(()->{
 table.put("key", "value1");
}).start();

new Thread(()->{
 table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的

  • 注意它们多个方法的组合不是原子的

线程安全类方法的组合

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

此时,就不是线程安全的了。

第一个线程使用get方法,但那之后可能该线程可能就不一定能立即执行put方法了。

QQ截图20220203191836

IntegerString 则是利用final修饰加上另外创建对应对象保证线程安全。

4.5.一些问题分析

例1:

public class  MyServlet extends HttpServlet {
 // 是否安全?否
 Map<String,Object> map = new HashMap<>();
 // 是否安全?是
 String S1 = "...";
 // 是否安全?是
 final String S2 = "...";
 // 是否安全?不是
 Date D1 = new Date();
 // 是否安全?不是,final只是不能改变应用
 final Date D2 = new Date();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
  // 使用上述变量
 }
} 

例2:

public class MyServlet extends HttpServlet {
 // 是否安全? 安全因为是局部变量且没有逃离作用范围
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 记录调用次数
 private int count = 0;
 
 public void update() {
 // ...
 count++;
 }
}

例3:

@Aspect
@Component
public class MyAspect {
 // 是否安全? 否 成员变量且有读写操作
 private long start = 0L;
 
 @Before("execution(* *(..))")
 public void before() {
 start = System.nanoTime();
 }
 
 @After("execution(* *(..))")
 public void after() {
 long end = System.nanoTime();
 System.out.println("cost time:" + (end-start));
 }
}

例4:

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao { 
 public void update() {
 String sql = "update user set password = ? where username = ?";
 // 是否安全
 try (Connection conn = DriverManager.getConnection("","","")){
 // ...
 } catch (Exception e) {
 // ...
 }
 }
}

安全 局部变量且没有脱离作用范围

例5:

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 // 是否安全
 private Connection conn = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

安全,局部变量且没脱离作用范围

例6

public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService { 
 public void update() {
 UserDao userDao = new UserDaoImpl();
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 // 是否安全
 private Connection = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

不安全 conn是成员变量,且有读写操作。

例7

public abstract class Test {
 
 public void bar() {
 // 是否安全
 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 foo(sdf);
 }
 
 public abstract foo(SimpleDateFormat sdf);
 
 
 public static void main(String[] args) {
 new Test().bar();
 }
}

不安全,因为foo是抽象方法无法保证不泄露局部变量,使它脱离作用域。

foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值