为什么要使用关键字synchronized?
程序的并行化是为了提高效率,但是不能以牺牲正确性为代价。
在java中volatile关键字不能真正的保证线程的安全性,volatile不能代替锁,它无法保证一些符合操作的原子性。它只能确保一个线程修改了数据之后,其他的线程能够看到这个改动,但是当两个线程同时修改某一个数据时,却会产生冲突。
下面为演示代码:
public class AccountingVol implements Runnable{
static AccountingVol instance=new AccountingVol();
static volatile int i=0;
public static void increase() {
i++;
}
public void run() {
for(int j=0;j<10000000;j++) {
increase();
}
}
public static void main(String[] aegs) throws InterruptedException{
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
运行后结果很多时候小于20000000,出现了类似的情况,线程就是不安全的。
关键字synchronized的作用就是实现线程的同步,他的工作是对同步的代码加锁,使的每一次,只有一个线程进入同步块,从而保证安全性。
synchronized 的基本用法
1.指定枷锁对象;对给定的对象枷锁,进入同步代码前要获得给定的对象的锁。
下面的代码,将synchronized作用于一个给定的对象instance,因此,每次线程进入synchronized包裹的代码段,就会要求请求instance的实例锁。如果有其他的线程正在访问这把锁,那么新到的线程就要等待。这样就保证了每次只有一个线程执行i++操作
package pconcurrent;
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
public void run() {
for(int j=0;j<10000000;j++) {
synchronized(instance){
i++;
}
}
}
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(i);
}
}
操作结果如下:
这样结果就是正确的了。
2.直接作用于实例方法:相当于对实例方法加锁,进入同步代码前要获得当前实例方法的锁。
代码如下:
public class AccountingSync2 implements Runnable{
static AccountingSync2 instance=new AccountingSync2();
static int i=0;
public synchronized void increase() {
i++;
}
public void run() {
for(int j=0;j<10000000;j++) {
increase();
}
}
public static void main(String[] aegs) throws InterruptedException{
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
结果也是正确的为20000000.
这里需要注意的是13,14行,这里使用Runnable接口创建两个线程,并且两个人线程都指向同一个Runnable接口实例(instance对象),所以才保证两个线程在工作是能够关注到同一个对象锁上去,保证了线程的安全。
下面为一种错误的方式:
public class AccountingSyncBad implements Runnable {
static int i=0;
public synchronized void increase() {
i++;
}
@Override
public void run() {
// TODO 自动生成的方法存根
for(int j=0;j<10000000;j++) {
increase();
}
}
public static void main(String[] args)throws InterruptedException{
Thread t1=new Thread(new AccountingSyncBad());
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
结果如下没有到达20000000.
错误的原因在于,虽然第三行申明了同步方法,但是创建线程时指向了两个不同的Runnable实例(new出来的对象,都会是新的对象),两个线程就不会关注到同一个对象锁上去。
3.直接作用于静态方法,相当于对当前类加锁,进入同步代码前要获得当前类的锁。
上述的错误代码有一种改正方法就是将关键字synchronized直接作用于静态方法,代码如下:
public class AccountingSync3 implements Runnable {
static int i=0;
public static synchronized void increase() {
i++;
}
public void run() {
for(int j=0;j<10000000;j++) {
increase();
}
}
public static void main(String[] args)throws InterruptedException{
Thread t1=new Thread(new AccountingSync3());
Thread t2=new Thread(new AccountingSync3());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
结果为:
这样做结果就是正确的了,及时两个线程指向不同的Runnable对象,但由于方法块需要的时当前类的锁,而非当前实例。因此线程还是可以同步的。
补充
出了用于线程的同步,确保线程外,synchronized还可以保证线程之间的可见性和有序性。可见性方面synchronized可以完全代替volatile的功能。有序性方面synchronized限制每次只有一个线程可以访问同步块,因此只要保证同步块内串行语意一致,那么执行的结果就是一样的。
最近在看书(java高并发程序设计)自学并发,为了加强记忆,博客记录之。