线程安全的概念
并发程序开发的一大关注重点就是线程安全。一般来说,程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序本身也就没有任何意义了。因此,线程安全就是并行程序的根本和根基。
问题示例
下面的代码演示一个计数器,两个线程同时对i进行累加操作,各执行1000000次。我们希望的执行结果当然是最终的i的值可以达到2000000,但事实并非总是像我们期望的那样。多次执行,大部分i的最终值会小于2000000。因为两个线程同时对i写入是,其中一个线程的结果会覆盖另外一个(虽然i被声明为volatile变量)
public class SynchronizedTest implements Runnable {
static SynchronizedTest instance = new SynchronizedTest();
static volatile int count = 0;
@Override
public void run() {
for (int i=0;i<1000000;i++){
count++;
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = [" + count + "]");
//count = [187898]
}
}
如果在代码中发生了类型的情况,这就是多线程不安全的恶果。线程1和线程2同时读取i为0,并各自计算得到i=1,并先后写入这个结果,因此,虽然i++被执行了两次,但是实际i的值只增加了1。
要从根本上解决这个问题,就必须保证多个线程在对i进行操作时完全同步。当线程1在写入时,线程2不但不能写,同时也不能读。在线程1写完之前,线程2读取的一定是一个过期的数据。Java中提供了重要的关键字synchronized来实现这个功能。
关键字synchronized有多种用法:
- 指定加锁对象: 对给定对象加锁,进入同步代码前要获得给定对象的锁
- 直接作用于实例方法: 相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
- 直接作用于静态方法: 相当于对当前类加锁,进入同步代码前要获得当前类的锁
1.指定加锁对象
public class SynchronizedTest implements Runnable {
static SynchronizedTest instance = new SynchronizedTest();
static int count = 0;
@Override
public void run() {
for (int i=0;i<1000000;i++){
synchronized (instance){
count++;
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = [" + count + "]");
}
}
2.直接作用于实例方法
public class SynchronizedTest implements Runnable {
static SynchronizedTest instance = new SynchronizedTest();
static int count = 0;
private synchronized void add(){
count++;
}
@Override
public void run() {
for (int i=0;i<1000000;i++){
add();
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = [" + count + "]");
}
}
直接作用于实例方法,为了保证线程间的正常同步,传入Thread的Runnable需是同一个实例对象。
3.直接作用于静态方法
public class SynchronizedTest implements Runnable {
static SynchronizedTest instance = new SynchronizedTest();
static int count = 0;
private static synchronized void add(){
count++;
}
@Override
public void run() {
for (int i=0;i<1000000;i++){
add();
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new SynchronizedTest());
Thread t2 = new Thread(new SynchronizedTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = [" + count + "]");
}
}
直接作用于静态方法,即使执行这段代码的两个实例指向了不同的Runnable对象,线程间还是可以正常同步。
synchronized除了用于线程安全同步、确保线程安全外,还可以保证线程间的可见性和有序性。从可见性的角度将,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。