Java 线程安全 与 锁

Java 线程安全 与 锁

多线程内存模型

  • 线程私有栈内存
    • 每个线程 私有的内存区域
  • 进程公有堆内存
    • 同一个进程 共有的内存区域

为什么会有线程安全问题?

  • 多个线程同时具有对同一资源的操作权限,又发生了同时对该资源进行读取、写入的情况,那么就会出现重复操作的情况

如何解决线程安全问题呢? 加锁

什么是锁?

锁就是对于操作资源的一种权限

锁可以做什么?

对于一个资源加锁后,每次只能有一个线程对该资源进行操作,当该线程操作结束后,才会解锁。
解锁之后,所有的线程获得竞争此资源的机会。

什么情况下需要加锁?

  • 读读 不需要加锁
  • 写写 需要加锁
  • 读写 需要加锁

加锁的两种方式(synchronized关键字与Lock对象)

第一种:synchronized关键字

  • 方法前加synchronized关键字

    • 功能:线程进入用synchronized声明的方法时就上锁,方法执行完自动解锁,锁的是当前类的对象
    • 调用synchronized声明的方法一定是排队运行的
    • 当A线程 调用object对象的synchronized声明的X方法时
      • B线程可以调用其他非synchronized声明的方法
      • B线程不能调用其他synchronized声明的非X方法
  • synchronized锁重入

    • 锁重入的概念:自己可以重复获得自己的内部锁。即synchronized声明的方法,可以调用本对象的其他synchronized方法。
    • 锁重入支持继承的环境,即子类的synchronized方法也可以调用父类的synchronized方法。
  • synchronized同步代码块

    • synchronized关键字与synchronized代码块的区别

      • synchronized声明的方法是将当前对象作为锁
      • synchronized代码块是将任意对象作为锁
    • 当两个线程访问同一个对象的synchronized代码块时,只有一个线程可以得到执行,另一个线程只能等待当前线程执行完才能执行。

      • 一半同步,一半异步
        • 不在synchronized代码块中就是异步执行,在synchronized代码块中就是同步执行

下面对“一半同步,一半异步”进行代码验证

  • 创建项目ltl0002 ,文件Task的代码如下:
package ltl0002;

public class Task {

    public void doTask(){
        for (int i = 0; i < 100; i++) {
            System.out.println("no synchronized ThreadName = " + Thread.currentThread().getName() + " i = " + (i+1));
        }
        synchronized (this){
            for (int i = 0; i < 100; i++) {
                System.out.println("synchronized ThreadName = " + Thread.currentThread().getName() + " i = " + (i+1));
            }
        }
        
    }
}
  • 两个线程类代码
package ltl0002;

public class MyThread1 implements Runnable{

    private Task task = new Task();

    public MyThread1(Task task){
        this.task = task;
    }

    @Override
    public void run() {

        task.doTask();
    }
}
package ltl0002;

public class MyThread2 implements Runnable{

    private Task task = new Task();

    public MyThread2(Task task){
        this.task = task;
    }

    @Override
    public void run() {

        task.doTask();
    }
}

文件Run.java代码如下:

package ltl0002;

public class Run {
    public static void main(String[] args) {
        Task task = new Task();
        MyThread1 myThread1 = new MyThread1(task);
        MyThread2 myThread2 = new MyThread2(task);
        Thread tr1 = new Thread(myThread1);
        Thread tr2 = new Thread(myThread2);
        tr1.start();
        tr2.start();
    }

}

程序运行结果如图所示
image

进入synchronized代码块之后,排队运行,运行结果如图所示
image

在第一张图我们可以看到,线程0 和 1交叉输出,说明是异步进行,而在第二张图可以看出线程0运行完之后,线程1才运行,说明它们是同步运行,验证完毕。

  • 现有三个线程,线程一对num进行修改,线程二三对num进行读取,如何可以实现,线程一与线程二三同步执行,而线程二三异步执行呢?
    现在创建项目ltl0003进行测试,Number文件代码如下
package ltl0003;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:53
 * @purpose 成员变量有int num,以及get set方法
 */
public class Number {
  private int num;
  private boolean change = false;

  public int getNum() {
    return num;
  }

  public void setNum(int num) {
    this.num = num;
  }
  public boolean isChangeing(){
    return change;
  }

  public void setChange(boolean change) {
    this.change = change;
  }
}

两个线程类的代码如下:

package ltl0003;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:36
 * @purpose 更改num的值
 */
public class MyThread01 implements Runnable{
  static int num = 0;
  Number number;
  public MyThread01(Number num ){
    this.number = num ;
  }
  @Override
  public void run() {
    synchronized (this){
      number.setChange(true);
      for (int i = 0; i < 10000; i++) {
        number.setNum(num++);
      }
      number.setChange(false);
    }
  }
}

package ltl0003;

import static java.lang.Thread.sleep;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:35
 * @purpose 读取num的值
 */
public class MyThread02 implements Runnable{
  Number number;

  public MyThread02(Number num ){
    this.number = num ;
  }

  @Override
  public void run() {
    for (int i = 0; i < 1000 ; i++) {
      //如果number正在更改,就休眠1ms
      while(number.isChangeing()){
        try {
          sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println(Thread.currentThread().getName()+"的输出为: num = " + number.getNum());
    }
  }

}

主函数文件Run代码如下:

package ltl0003;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:15
 * @purpose 解决锁问题 线程一对num进行修改,线程二三对num进行读取,此代码要实现:线程一与线程二三同步执行,而线程二三异步执行。
 */
public class Run {
  public static void main(String[] args) {
    Number number = new Number();
    number.setNum(0);
    MyThread01 myThread01 = new MyThread01(number);
    MyThread02 myThread02 = new MyThread02(number);
    Thread tr1 = new Thread(myThread01);
    Thread tr2 = new Thread(myThread02);
    Thread tr3 = new Thread(myThread02);
    tr1.start();
    tr2.start();
    tr3.start();
  }
}

实验结果如图所示

image

我们发现,线程2/3执行的时候,线程1已经执行完毕,且线程2、3异步进行。

第二种:Lock对象的使用

  • ReentrantLock类可以达到与synchronized同样的效果。
  • 用法:
ReentrantLock lock = new ReentrantLock (); 
lock.lock();//加锁
lock.unlock();//解锁
        
//使用try catch finally 可以确保finally 中的代码执行,在finally中解锁
try{
    while(true){
        lock.lock ();
        //操作代码
    }
}catch (Exception e) {
    e.printStackTrace();
}finally {
    lock.unlock ();
}
posted @ 2022-04-23 19:09  classic123  阅读(325)  评论(0编辑  收藏  举报