多个线程共享一个变量的时候,会出现线程安全的问题,这是大家都知道的问题,那么是为什么呢?每一个线程都有自己的工作空间,当创建一个线程的时候,系统会为这个线程分配相应的空间,共享的变量也会在这个空间里。然后每个线程在各自的空间执行,并修改共享的变量。当多个线程同时修改一个变量的时候,这时候就可能会出现数据不一致的问题。下面举例说明:
public class Test {
private static int count;
public static void main(String[] args) {
for (int i = 0; i < 10000;i++) {
Thread thread = new Thread(){
@Override
public void run() {
count++;
System.out.println(count);
}
};
thread.start();
}
}
上面代码为,开启1000个线程,每个线程对count进行加1操作。按照预期,最后一个输出的count应该是10000才对。但事实上并不是,看下面输出结果:
9992
9993
9994
9995
9996
9997
由此可见,最后一个输出的是9997并不是10000。这就是一个典型的线程安全问题,导致结果达不到我们的预期。
为了解决线程安全问题,java提供了锁的机制。当线程访问被锁到的对象时,同一时间只有一个线程能够得到锁,其他线程被阻塞,只有锁被释放时,其他线程才有机会去获得锁。java是通过synchronized关键字为对象加锁的,synchronized可以修饰代码块,方法,类,静态方法。接下来分别举例说明下用法:
1.synchronized修饰代码块:同一时间只有一个线程能够执行代码块中的内容。
package volatile1;
/**
* Created by sxb-gt on 2018/1/21.
* 辅助线程
*/
public class AssistantThread extends Thread {
private MainThread mainThread;
public AssistantThread(MainThread mainThread) {
this.mainThread = mainThread;
}
@Override
public void run() {
mainThread.add();
mainThread.getCount().countDown();
}
}
package volatile1; import java.util.concurrent.CountDownLatch; /** * Created by sxb-gt on 2018/1/21. * “主线程类”注意:该类并没有集成Thread。只是为了形象,才取的这个名字。 */ public class MainThread { private Integer i = 0; /** 线程计数器 **/ private CountDownLatch count; public CountDownLatch getCount() { return count; } public void setCount(CountDownLatch count) { this.count = count; } public void add() { System.out.println("线程名称:" + Thread.currentThread().getName() + "执行开始"); synchronized(i) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } i++; System.out.println(i); } System.out.println("线程名称:" + Thread.currentThread().getName() + "执行结束"); } public static void main(String[] args) { System.out.println("主线程执行开始"); MainThread mainThread = new MainThread(); int length = 3; mainThread.setCount(new CountDownLatch(length)); for (int i = 0; i < length; i++ ) { Thread thread = new AssistantThread(mainThread); thread.start(); } try { mainThread.getCount().await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("i===" + mainThread.i); System.out.println("主线程执行结束"); } }
输出结果:
主线程执行开始 线程名称:Thread-2执行开始 线程名称:Thread-1执行开始 线程名称:Thread-0执行开始 1 线程名称:Thread-2执行结束 2 线程名称:Thread-0执行结束 3 线程名称:Thread-1执行结束 i===3 主线程执行结束
结论:三个线程同时开启,都执行到方法代码块的时候,只有一个线程可以进入方法代码块。
2.synchronized修饰方法:同一时间只有一个线程能够进入该方法。
上面例子中的代码不变,只改变add()方法。将代码块中的synchronized拿走,在方法声明上加上synchronized。
public synchronized void add() {
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行开始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println(i);
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行结束");
}
执行结果:
主线程执行开始
线程名称:Thread-0执行开始
1
线程名称:Thread-0执行结束
线程名称:Thread-1执行开始
2
线程名称:Thread-1执行结束
线程名称:Thread-2执行开始
3
线程名称:Thread-2执行结束
i===3
主线程执行结束
结论:从执行结果可以看出同一时间只有一个线程进入了同步方法。相对于修饰代码块,线程阻塞的更早。因此,个人建议,需要同步的时候优先使用阻塞代码块的方式。
3.synchronized修饰类:该类的所有实例,只有一个对象能够执行同步方法。
首先,我们调整下代码,如下:
package volatile1;
import java.util.concurrent.CountDownLatch;
/**
* Created by sxb-gt on 2018/1/21.
* “主线程类”注意:该类并没有集成Thread。只是为了形象,才取的这个名字。
*/
public class MainThread1 extends Thread{
/** 线程计数器 **/
private static CountDownLatch count;
private static Integer i = 0;
@Override
public void run() {
add();
count.countDown();
}
public synchronized void add() {
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行开始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println(i);
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行结束");
}
public static void main(String[] args) {
System.out.println("主线程执行开始");
int length = 3;
count = new CountDownLatch(length);
for (int i = 0; i < length; i++ ) {
Thread thread = new MainThread1();
thread.start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i===" + i);
System.out.println("主线程执行结束");
}
}
此时,MainThread1就是一个线程,创建3个MainThread1的实例。然后执行,结果如下:
主线程执行开始
线程名称:Thread-1执行开始
线程名称:Thread-2执行开始
线程名称:Thread-0执行开始
1
线程名称:Thread-1执行结束
2
2
线程名称:Thread-0执行结束
线程名称:Thread-2执行结束
i===2
主线程执行结束
问题来了,线程并没有同步执行。为什么呢? 因为我们创建了三个对象,虽然在add()方法上加了synchronized,但是这个锁是对象级的,多个对象之间是不共享这个锁的。如何才能让所有的对象共享一个锁呢?接下来,再修改下代码,如下:
public void add() {
synchronized (MainThread1.class) {
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行开始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println(i);
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行结束");
}
}
输出结果:
主线程执行开始
线程名称:Thread-1执行开始
1
线程名称:Thread-1执行结束
线程名称:Thread-0执行开始
2
线程名称:Thread-0执行结束
线程名称:Thread-2执行开始
3
线程名称:Thread-2执行结束
i===3
主线程执行结束
线程之间再次同步。
结论:synchronized修饰类时,作用的是该类所有的实例。所有的实例,共享一个锁。但是不建议这样做,对系统性能会有影响。
4.synchronized修饰静态方法:与修饰类效果相同,代表所有实例共享这一个方法的锁。也可用与多个对象之间的同步。
调整代码如下:
public synchronized static void add() {
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行开始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println(i);
System.out.println("线程名称:" + Thread.currentThread().getName() + "执行结束");
}
输出结果:
主线程执行开始
线程名称:Thread-1执行开始
1
线程名称:Thread-1执行结束
线程名称:Thread-0执行开始
2
线程名称:Thread-0执行结束
线程名称:Thread-2执行开始
3
线程名称:Thread-2执行结束
i===3
主线程执行结束
结论:静态方法也叫类方法,只在类第一次实例化的时候加载一次。因此,使用synchronized修饰静态方法也可以保证多个对象之间的同步。