Java高并发学习(6)
线程安全的概念与synchronized
并行程序开发的一大关注点是线程安全问题。由于读写者问题产生的错误,会导致数据不一致。虽然在使用volatile关键字后这种错误情况有所改善。但是,volatile并不能真正的保证线程安全。他只能保证一个线程修改数据后其他线程能看到这个改动。但当两个线程同时修改一个数据时,依然会产生冲突。
下面代码演示了一个计数器,两个线程同时对i进行累加操作,个执行100000次。我们希望当两个线程执行结束后i的值为200000,但事实往往并非如此。如果你多次执行下面的代码,你会发现i的值总是小于200000。这就是两个线程同时对i写入,其中一个线程结果会覆盖另一个(虽然这时候i被声明为volatile变量)。
public class fist{
static int i =0;
public static class MyThread_Write extends Thread{
@Override
public void run(){
for(int j=0;j<100000;j++){
i++;
}
}
}
public static void main(String args[]) throws InterruptedException {
MyThread_Write t1 = new MyThread_Write();
MyThread_Write t2 = new MyThread_Write();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
3次输出结果如下:
上述结果产生的原因:假设线程1和线程2同时读取i为0,并各自计算得到了i=1,并先后写入到这个结果,因此,虽然i++被执行了两次,但是实际i的值只增加了1。
要从根本上解决这个问题,我们就要保证多个线程在对i进行操作时完全同步,也就是说,当A线程在写入时,线程B不仅不能写入,同时也不能读。因为在线程A写完之前,线程读取的一定是一个过期数据。Java中提供了一个重要的关键字synchronized来实现这个功能。
关键字synchronized关键字的作用是实现线程间的同步。他的工作是对同步代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程将的安全性。
关键字synchronized可以有多重种用法。这里做一个简单的整理:
·指定加锁对象:对给定对象加锁,进入同步代码前要获得指定的锁。
·直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
·直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
下面代码,将synchronized作用于本类,因此,没当线程进入synchronized包裹的代码段,就都会要求请求fist.class的锁。如果其他线程持有这把锁,那么新到的线程就必须等待。这样,就保证了每一次只能有一个线程进行i++操作。
public class fist{
static int i =0;
public static class MyThread_Write extends Thread{
@Override
public void run(){
synchronized (fist.class) {
for(int j=0;j<100000;j++){
i++;
}
}
}
}
public static void main(String args[]) throws InterruptedException {
MyThread_Write t1 = new MyThread_Write();
MyThread_Write t2 = new MyThread_Write();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
执行结果:
当然,也可以让关键字作用于一个实例方法。这就是对对象中的方法加锁,但这里先给出一种错误的加锁方式。
public class fist{
static int i =0;
public static class MyThread_Write extends Thread{
public synchronized void increase(){
i++;
}
@Override
public void run(){
for(int j=0;j<100000;j++){
increase();
}
}
}
public static void main(String args[]) throws InterruptedException {
MyThread_Write t1 = new MyThread_Write();
MyThread_Write t2 = new MyThread_Write();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
上述代码就犯了个严重的错误。虽然声明了increase()方法是一个同步方法。但很不幸的是,这段代码指向的是不同的increase()方法,这是什么意思呢?其实我们在创建线程t1和t2时分别用了两次new MyThread_Write(),然后java虚拟机在内存中创建了两个 MyThread_Write对象,这两个对象都有increase()方法,也就是说,在内存中有两个increase()方法。然而我们只让每一个对象对自己的increase()方法上了锁。
那怎么解决这个问题呢?我们会想要是内存中只有一个increase()方法就好了,无论我们创造多少个 MyThread_Write对象,他们的increase()方法都指向内存中唯一的increase()方法就好了!那这不就是静态方法吗!其实我们只要把increase()方法声明为静态方法就好了。
public class fist{
static int i =0;
public static class MyThread_Write extends Thread{
public static synchronized void increase(){
i++;
}
@Override
public void run(){
for(int j=0;j<100000;j++){
increase();
}
}
}
public static void main(String args[]) throws InterruptedException {
MyThread_Write t1 = new MyThread_Write();
MyThread_Write t2 = new MyThread_Write();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
除了用于线程同步,确保线程的安全之外,synchronized还可以确保线程间的可见性和有序性。从可见性的角度上讲,synchronized完全可以代替volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized每一次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行的结果总是一样的。换而言之,被synchronized限制的多个线程是串行执行的。