什么是线程安全?
什么是线程安全了?当多个线程并发访问某个java对象的时候。无论cpu按什么样的顺序调度这些线程,得到的结果都是一样的,就是线程安全的;反之,则是线程不安全的。
这里我们来讨论一个线程不安全的案例,通过多个线程对一个整数进行自增操作。
我们启动两个线程A、B;A线程对整数自增10万次。B线程对整数自增10万次;假设整数的初始值为0,那么当A、B自增结束之后,整数的值应该是20万。那么事实究竟是否如此了?我们通过程序验证一下:
public class Demo003_a {
public static void main(String[] args) {
new Demo003_a().testAddUnsafe();
}
public Integer counter = 0; //初始值为0
/**
* @desc 测试多线程累加
* */
public void testAddUnsafe(){
Runnable run = new Runnable() {
@Override
public void run() {
//累加1万次
for(int i=0;i<10000;i++)
counter++;
}
};
//启动2个线程进行累加
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t1.start();
t2.start();
//等待t1、t2运行结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
第一次运行结果:14860
第二次运行结果:10000
结果是显而易见的,自增运算并不是线程安全的。why?
自增运算看起来是一个原子操作。然而事实上,自增运算符是一个复合操作。它至少包含3个JVM指令(内存取值、寄存器加1、存值到内存)。这里我们简单描述一下其出现错误的原因。
时间 |线程A | 线程B
|1、从内存读取变量的值; |
|2、寄存器执行加1指令; |1、从内存读取变量的值;
|3、cpu时间片使用完; | 2、寄存器执行加1指令;
|4、阻塞中...; | 3、将寄存器结果写入内存;
|5、竞争到cpu时间片,将寄存器结果写入内存; |
上面列出了A、B线程执行过程中,可能发生的一种情况,很明显。线程A的结果覆盖了线程B的结果。
synchronized(内置锁) 关键字
有什么办法可以解决上述线程不安全的情况了? 对不安全的指令进行加锁是经常使用的解决办法。以上面的例子为例,我们可以对counter++指令进行加锁,即需要获得锁,才可以执行该指令。这样就保证了同一个时刻,只有一个线程处理加锁的指令,从而保证了线程安全。
synchronized 是java对象的内置锁,每个java对象都有一把内置锁。关于内置锁的详细原理,可以在 “java多线程_内置锁02_对象结构和内置锁” 一文中详细了解。在此,我们只需要知道,通过内置锁可以保证锁定的指令在同一时刻只有一个线程去执行。
java中,synchronized 内置锁可以加在方法、静态方法、代码块中。
synchronized 保护方法
在编写java方法的时候加上synchronized关键字,则该方法同一时刻,只能由一个线程执行。
public class Demo003_a {
public static void main(String[] args) {
new Demo003_a().testSynchronized1();
}
public Integer counter = 0; //初始值为0
/**
*
* @desc 同步方法测试
* */
public void testSynchronized1(){
Runnable run = new Runnable() {
@Override
public void run() {
add();
}
//同步方法,使用匿名内部类的对象的锁(即run对象的内置锁)
public synchronized void add(){
//累加1万次
for(int i=0;i<10000;i++)
counter++;
}
};
//启动2个线程进行累加
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t1.start();
t2.start();
//等待t1、t2运行结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
可以观察到,不论如何运行程序;最终累加的结果都是 2000.
synchronized 保护代码块
在方法上面加锁的缺点是,整个方法中的指令都会被限制为单线程访问。这无疑会降低系统的性能——可能有些代码并不需要安全保护。为了提高系统的性能,我们要尽可能减少锁定的范围。
在代码块上面加锁,是比较优雅的一种做法。
public class Demo003_a {
public static void main(String[] args) {
new Demo003_a().testSynchronized1();
}
public Integer counter = 0; //初始值为0
/**
*
* @desc 同步方法测试
* */
public void testSynchronized1(){
Runnable run = new Runnable() {
@Override
public void run() {
//处理其它的事情...
//同步方法,使用匿名内部类的对象的锁(即run对象的内置锁)
synchronized(this) {
//累加1万次
for(int i=0;i<10000;i++)
counter++;
}
//处理其它的事情...
}
};
//启动2个线程进行累加
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t1.start();
t2.start();
//等待t1、t2运行结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
上面的 synchronized(this) 表示使用当前对象的内置锁。
synchronized 保护静态方法
静态方法和普通方法的区别在于,静态方法属于 Class 类的实例(Class类的实例是类加载器在加载类到JVM虚拟机的时候创建的),而普通方法是属于当前编写的类的实例。所以synchronized加在静态方法上面,则使用的是Class实例的内置锁。
使用方法和一般方法一摸一样。
public static Integer counter = 0; //初始值为0
public static synchronized void add(){
for(int i=0;i<10000;i++)
counter++;
}