目录
前言:
对线程的基本了解已经结束,除此之外,Java多线程编程中还有保证线程安全的内容。
对于多线程编程,线程安全无疑是最重要的。监视器锁是保证线程安全的重要部分。
序列:多线程 - 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。这便是一个线程不安全的示例,运行结果如下:
1.3多线程不安全的原因
(1)线程的调度是“随机的”:这是多线程编程的罪魁祸首。随机调度使一个程序在多线程环境下,执行的顺序存在很多的变数。程序员必须保证在任意执行顺序下,代码都能正常工作。
(2)修改共享数据:多线程程序在运行时,对于相同的数据变量,会修改数据变量 ,影响多线程的安全性。
(3)不保证原子性:多线程程序运行时,对一个操作不会保证原子性,从而影响多线程的安全性。一条java语句不一定是原子的,比如n++操作分三步完成:
- 从内存把数据读取到CPU;
- 进行数据更新;
- 把数据写回到CPU。
(4)不保证可见性:一个线程对共享变量的修改,不能被其他的线程及时的看到。
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修饰的代码块时,相当于“解锁”;
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(){
//对整个静态方法加锁
}
}
以上便是Java多线程编程中监视器锁的介绍。