监视器锁-synchronized关键字

目录

1.多线程编程安全

1.1多线程安全概念

1.2多线程不安全实例

1.3多线程不安全的原因

1.4解决以上线程不安全的示例

2.synchronized关键字(重点)

2.1synchronized关键字概念

2.2synchronized关键字特性

2.2.1互斥特性

2.2.2可重入特性

2.3synchronized使用示例

2.3.1修饰代码块

2.3.2直接修饰普通方法

2.3.3修饰静态方法


序列:多线程 - 005

1.多线程编程安全

1.1多线程安全概念

如果多线程环境下代码的运行结果是符合我们预期的,即在单线程环境下应该的结果,则说这个多线程程序是安全的。

1.2多线程不安全实例

以下代码创建出了两个对n变量都自增5000次的线程,然后main线程等待其都自增完毕再进行输出n的值。

public class Main {
    public static int n = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread01 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {//对n变量自增5000次
                Main.n++;
            }
        });
        Thread thread02 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {//对n变量自增5000次
                Main.n++;
            }
        });
        thread01.start();//启动两个自增线程
        thread02.start();
        thread01.join();//main线程等待thread01线程自增完成后,再执行
        thread02.join();//同上
        System.out.println("n的值为:" + Main.n);
    }
}

可以预期到,我们想要的n的值,最后输出应该是10000,这是一个多线程安全的结果。但是这个程序的运行结果和我们预期的值却大相径庭,为8964。这便是一个线程不安全的示例,运行结果如下:

监视器锁-synchronized关键字_第1张图片

1.3多线程不安全的原因

(1)线程的调度是“随机的”,这是多线程编程的罪魁祸首。随机调度使一个程序在多线程环境下,执行的顺序存在很多的变数。程序员必须保证在任意执行顺序下,代码都能正常工作。

(2)修改共享数据,多线程程序在运行时,对于相同的数据变量,会修改数据变量 ,影响多线程的安全性。

(3)原子性,多线程程序运行时,对一个操作不会保证原子性,从而影响多线程的安全性。

1.4解决以上线程不安全的示例

以下代码输出结果为10000,符合多线程编程的安全性。其中使用到的synchronized关键字,接着会进行重点介绍。

public class Main {
    public static int n = 0;
    public static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread01 = new Thread(()->{
            synchronized (object){
                for (int i = 0; i < 5000; i++) {//对n变量自增5000次
                    Main.n++;
                }
            }
        });
        Thread thread02 = new Thread(()->{
            synchronized (object){
                for (int i = 0; i < 5000; i++) {//对n变量自增5000次
                    Main.n++;
                }
            }
        });
        thread01.start();//启动两个自增线程
        thread02.start();
        thread01.join();//main线程等待thread01线程自增完成后,再执行
        thread02.join();//同上
        System.out.println("n的值为:" + Main.n);
    }
}

2.synchronized关键字(重点)

2.1synchronized关键字概念

为了解决多线程编程的不安全,从而引入了编程器锁,synchronized关键字。

对于以上的多线程自增代码示例,synchronized关键字可以先将thread01每一次自增的代码进行加锁,此时thread02线程无法对n变量进行修改操作,保证了thread01线程的原子性,也保证了thread02线程不会修改共享数据n;反之对于thread02线程加锁,效果相同。

2.2synchronized关键字特性

2.2.1互斥特性

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一对象的synchronized就会阻塞等待。

  • 进入synchronized修饰的代码块时,相当于“加锁”;
  • 退出synchronized修饰的代码块时,相当于“解锁”;

监视器锁-synchronized关键字_第2张图片

2.2.2可重入特性

synchronized同步块对同⼀条线程来说是可重⼊的,不会出现自己把自己锁死的问题;

锁死问题:⼀个线程没有释放锁,然后又尝试再次加锁。

  • 第⼀次加锁,加锁成功,lock();
  • 第⼆次加锁,锁已经被占⽤,阻塞等待,lock();

Java 中的 synchronized 是可重入锁, 因此没有上⾯的“锁死问题”。以下代码不会影响自增结果为10000。

Thread thread01 = new Thread(()->{
            synchronized (object){
                synchronized (object){
                    for (int i = 0; i < 5000; i++) {//对n变量自增5000次
                        Main.n++;
                    }
                }
            }
        });

在可重⼊锁的内部,包含了 "线程持有者" 和 "计数器" 两个信息。

  • 如果某个线程加锁的时候,发现锁已经被⼈占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增;
  • 解锁的时候计数器递减为 0 的时候,才真正释放锁。(才能被别的线程获取到)

2.3synchronized使用示例

基本使用:synchronized本质上要修改指定对象的 "对象头"。从使用角度来看,synchronized 也势必要搭配⼀具体的对象来使⽤。这个“对象头”可以是任意对象,只要符合逻辑即可。

2.3.1修饰代码块

可以明确指定锁哪个对象。

锁任意对象:

public class Main {
    public static Object object = new Object();
    public void method(){
        synchronized (object){
            //这里是锁的内容,为任意对象
        }
    }
}

锁当前对象:

public class Main {
    public void method(){
        synchronized (this){
            //这里是锁的内容,为当前对象
        }
    }
}
2.3.2直接修饰普通方法
public class Main {
    public synchronized void method(){
        //对整个普通方法加锁
    }
}
2.3.3修饰静态方法
public class Main {
    public synchronized static void method(){
        //对整个静态方法加锁
    }
}

你可能感兴趣的:(JavaEE,(初阶),java,开发语言,java-ee)