线程的原子性是指:一个或者一系列指令,它的操作是不可中断的,一旦开始是不允许被其他CPU或线程来中断。
我们来看下述代码:ThreadAtomicityDemo
类中有个初始值为0的Integer类型的静态变量count
,还有一个每次sleep一毫秒,并且使count加一的静态方法increase
。
在main方法中,循环1000次,启动1000个线程来执行 increase方法。
请问在执行过1000个线程,每次使count加一后,count是否为1000?
public class ThreadAtomicityDemo {
public static int count = 0;
public static void increase(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->ThreadAtomicityDemo.increase()).start();
}
Thread.sleep(2000);
System.out.println("The count is : " + count);
}
}
执行结果:
The count is : 984
Process finished with exit code 0
结果是小于1000的随机数。
上述代码的原子性体现在 count++
上。
count++ 虽然只是一行代码,但实际上在底层执行CPU指令时,count++ 会转化为三个指令。我们一起来看一下:
在class目录下执行 D:\open_source\MyBatis\MyThread\target\classes\demo>javap -v ThreadAtomicityDemo.class
,
可以看到字节码中的 count++ 指令,节选关键部分如下:
...
public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: lconst_1
1: invokestatic #2 // Method java/lang/Thread.sleep:(J)V
4: goto 12
7: astore_0
8: aload_0
9: invokevirtual #4 // Method java/lang/InterruptedException.printStackTrace:()V
12: getstatic #5 // Field count:I
15: iconst_1
16: iadd
17: putstatic #5 // Field count:I
20: return
Exception table:
from to target type
0 4 7 Class java/lang/InterruptedException
LineNumberTable:
line 7: 0
line 10: 4
line 8: 7
line 9: 8
line 11: 12
line 12: 20
LocalVariableTable:
Start Length Slot Name Signature
8 4 0 e Ljava/lang/InterruptedException;
StackMapTable: number_of_entries = 2
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
......
我们可以发现,count++在Java里表现出来的指令和CPU真正执行时的指令时有差距的。
由于CPU层面是由三条语句来完成count++,所以中多线程的情况下,就很难保证它的原子性了。
如上图所示,
- 当Thread-1开始执行,读取count的值0;
- 线程切换至Thread-2,读取count的值,当前还是0;
- 线程2继续执行count++,得到count的值为1,写入寄存器中;
- 这是线程切换回Thread-1,线程1从刚才中断的地方继续,count++,得到count值也是1
- Thread1将count=1 写入寄存器。
- 当前经过Thread1和Thread2两个线程执行后,count值最终为1,并非是2.
同理,1000个线程结束后,count值就为一个小于1000 的数。
如何保证多线程中的原子性,可以使用锁。
比如 Synchronized
关键字来实现的锁。
Synchronized 在使用的两种形态:
- 加在方法上
- 加在代码块上
//加在方法上的synchronized
synchronized void demo(){
// do something ..
}
void methodDemo(){
//synchronized代码块
synchronized(obj){
//do something..
}
}
锁的范围:(我要保护的资源的范围)
- 实例锁,范围是在当前对象实例里面。只针对当前对象实例有效。
- 类锁,范围是静态方法、类对象。针对所有的类对象都互斥。
public class SynchronizedDemo {
//1 实例锁,加在方法上
synchronized void demoMethod1(){
System.out.println("this is demo method1");
}
//2 实例锁,代码块
void demoMethod2(){
synchronized (this){
System.out.println("this is demo method2");
}
}
//3 类锁,加在方法上
synchronized static void demoMethod3(){
System.out.println("this is demo method3");
}
//4 类所,代码块
void demoMethod4(){
synchronized (SynchronizedDemo.class){
System.out.println("this is demo method4");
}
}
public static void main(String[] args) {
//实例锁生效,在同一个对象范围内互斥
SynchronizedDemo demo = new SynchronizedDemo();
new Thread(()->{
demo.demoMethod1();
},"t1").start();
new Thread(()->{
demo.demoMethod1();
},"t2").start();
//类锁生效,在同一个类范围互斥
SynchronizedDemo d1 = new SynchronizedDemo();
SynchronizedDemo d2 = new SynchronizedDemo();
new Thread(()->{
d1.demoMethod4();
},"d1").start();
new Thread(()->{
d1.demoMethod4();
},"d2").start();
}
}
比如对一个共享文件的修改,当一个线程修改时,对其加锁,其他线程无法修改。
互斥锁的本质,就是对共享资源的保护,只允许一个线程对其操作。
我们来通过使用锁,对一开始的代码进行线程安全处理:
public class ThreadAtomicityDemo {
public static int count = 0;
public static void increase(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized{ThreadAtomicityDemo.class){
count ++;
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->ThreadAtomicityDemo.increase()).start();
}
Thread.sleep(2000);
System.out.println("The count is : " + count);
}
}
代码执行结果:
The count is : 1000
Process finished with exit code 0