目录
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
如果多线程环境下代码的运行结果是符合我们预期的,即在单线程环境下应该的结果,则说这个多线程程序是安全的。
以下代码创建出了两个对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)线程的调度是“随机的”,这是多线程编程的罪魁祸首。随机调度使一个程序在多线程环境下,执行的顺序存在很多的变数。程序员必须保证在任意执行顺序下,代码都能正常工作。
(2)修改共享数据,多线程程序在运行时,对于相同的数据变量,会修改数据变量 ,影响多线程的安全性。
(3)原子性,多线程程序运行时,对一个操作不会保证原子性,从而影响多线程的安全性。
以下代码输出结果为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);
}
}
为了解决多线程编程的不安全,从而引入了编程器锁,synchronized关键字。
对于以上的多线程自增代码示例,synchronized关键字可以先将thread01每一次自增的代码进行加锁,此时thread02线程无法对n变量进行修改操作,保证了thread01线程的原子性,也保证了thread02线程不会修改共享数据n;反之对于thread02线程加锁,效果相同。
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一对象的synchronized就会阻塞等待。
synchronized同步块对同⼀条线程来说是可重⼊的,不会出现自己把自己锁死的问题;
锁死问题:⼀个线程没有释放锁,然后又尝试再次加锁。
Java 中的 synchronized 是可重入锁, 因此没有上⾯的“锁死问题”。以下代码不会影响自增结果为10000。
Thread thread01 = new Thread(()->{
synchronized (object){
synchronized (object){
for (int i = 0; i < 5000; i++) {//对n变量自增5000次
Main.n++;
}
}
}
});
在可重⼊锁的内部,包含了 "线程持有者" 和 "计数器" 两个信息。
基本使用:synchronized本质上要修改指定对象的 "对象头"。从使用角度来看,synchronized 也势必要搭配⼀个具体的对象来使⽤。这个“对象头”可以是任意对象,只要符合逻辑即可。
可以明确指定锁哪个对象。
锁任意对象:
public class Main {
public static Object object = new Object();
public void method(){
synchronized (object){
//这里是锁的内容,为任意对象
}
}
}
锁当前对象:
public class Main {
public void method(){
synchronized (this){
//这里是锁的内容,为当前对象
}
}
}
public class Main {
public synchronized void method(){
//对整个普通方法加锁
}
}
public class Main {
public synchronized static void method(){
//对整个静态方法加锁
}
}