这章节主要讲述的是跟线程安全相关的几个方法,例如Synchronized关键字、wait()/notify()、volatile、ThreadLocal.
synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。对于单虚拟机的并发编程来说,Synchronized是一个不错的解决并发编程的方法
Synchronized原理:
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
Synchronized的使用:
1)普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁(不同实例对象是不同的锁)
2)静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3)同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
1.1Synchronized修饰同步方法
①多个线程访问同一对象的同步方法和多个对象访问不同对象的同步方法比较
示例代码:
public class SynchronizedTest implements Runnable{
//共享资源
static int resource=200;
public synchronized void consumResouce(){
resource = resource-10;
}
@Override
public void run() {
for(int i=0;i<10;i++){
try {
Thread.sleep(10);
}catch (InterruptedException ex){
}
consumResouce();
}
}
public static void main(String []args) throws InterruptedException{
SynchronizedTest synchronizedTest = new SynchronizedTest();
//线程A和线程B的是同一个对象
Thread threadA = new Thread(synchronizedTest);
Thread threadB = new Thread(synchronizedTest);
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("两个进程运行完的结果resource="+resource);
//线程A和线程B的访问不同的两个对象
resource =200;
SynchronizedTest synchronizedTest2 = new SynchronizedTest();
Thread threadC = new Thread(synchronizedTest);
Thread threadD = new Thread(synchronizedTest2);
threadC.start();
threadD.start();
threadC.join();
threadD.join();
System.out.println("两个进程运行完的结果resource="+resource);
}
}
运行结果:
结果分析:
两个进程访问同一对象的同步实例方法,当前一个进程获取到锁时,后一个进程必须阻塞等待,同一个对象,相互之间会产生影响。所以最后资源肯定是为0
两个进程访问同不同对象的同步实例方法,当前一个进程获取到锁时,后一个进程无需阻塞等待,因为两个对象分别具有一把锁,锁的是不同对象,相互之间不会产生影响。所以最后运行结果是不确定的。例如20,或者40,30等。
②一个线程获取了该对象的锁之后,其他线程来访问其他synchronized实例方法或者非synchronized方法现象
示例代码:
public class SynchronizedMoreMthodTest {
public synchronized void method1(){
try{
System.out.println("我开始处理方法1");
Thread.sleep(3000);
System.out.println("我已完成方法1");
}catch(InterruptedException ex){
}
}
public synchronized void method2(){
System.out.println("我已完成方法2");
}
public void method3(){
System.out.println("我已完成方法3");
}
public static void main(String []args){
//创建一个实例对象
final SynchronizedMoreMthodTest synchronizedMoreMthodTest = new SynchronizedMoreMthodTest();
//线程A去执行方法1
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronizedMoreMthodTest.method1();
}
});
//线程A去执行方法1
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronizedMoreMthodTest.method2();
}
});
//线程A去执行方法1
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronizedMoreMthodTest.method3();
}
});
threadA.start();
//这里等待线程A1s的时间,目的是为了让线程A可以拿到cpu的时间,获取到锁
try {
threadA.join(1000);
}catch (InterruptedException ex){
}
threadB.start();
threadC.start();
}
}
运行结果:
运行结果分析:
当一个线程获取了一个实例方法的对象锁,当其它线程访问该对象的其它同步实例方法时会阻塞,必须等上一个线程释放锁后,才能继续访问;但可以直接访问其它的非同步实例方法。
1.2Synchronized修饰类方法
代码示例:
public class SynchronizedStaticMethodTest implements Runnable {
//共享资源
static int resource=200;
public synchronized static void consumResouce(){
resource = resource-10;
}
@Override
public void run() {
for(int i=0;i<10;i++){
//这里休息10ms,是为了让线程之间可以循环的使用时间片
try {
Thread.sleep(10);
}catch (InterruptedException ex){
}
consumResouce();
}
}
public static void main(String []args) throws InterruptedException{
Thread threadC = new Thread(new SynchronizedStaticMethodTest());
Thread threadD = new Thread(new SynchronizedStaticMethodTest());
threadC.start();
threadD.start();
threadC.join();
threadD.join();
System.out.println("两个进程访问不同对象运行完的结果resource="+resource);
}
}
运行结果:
运行结果分析:
两个线程访问不同对象的类同步方法是相互影响的,所以运行结果一定为0。
主要介绍Synchronized的使用,后续会有一个章节把Synchronized关键字与Lock锁进行对比,做一个讲解
2 . wait()/notify()
wait(),notify()和notifyAll()都是Object中的方法。
用法意义:
对同一对象的操作,类似于一个开关。具体体现到方法上则是这样的:一个线程A调用了对象obj的wait方法进入到等待状态,而另一个线程调用了对象obj的notify()或者notifyAll()方法,线程A收到通知后从对象obj的wait方法返回,继续执行后面的操作。而且方法必须用synchronized修饰,否则会抛 IllegalMonitorStateException异常。
使用这种等待唤醒机制的标准范式如下:
等待方:
通知方来说
对于notify和notifyAll()我们一般使用的是notifyAll(),为了防止有些通知丢失:
notify:随机唤醒一个等待该对象监视器的线程,具有不定性
notifyAll:唤醒所有的等待该对象监视器的线程
示例代码:
本示例模拟一次快递业务,当快递的里程数发生变化时,就会通知其他线程去做自己业务的操作,例如写数据库。
本文先定义一个快递类Express:
public class Express {
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public synchronized void changeKm(){
this.km = 101;
notifyAll();
//其他的业务代码
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public synchronized void changeSite(){
this.site = "BeiJing";
notify();
}
public synchronized void waitKm() {
System.out.println(Thread.currentThread().getName()+":enter waitKm");
while(this.km<=100) {
try {
wait();
System.out.println(Thread.currentThread().getName()+":check km is be notifed.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":the km is"+this.km+",I will change db.");
}
public synchronized void waitSite(){
System.out.println(Thread.currentThread().getName()+":enter waitSite");
while(CITY.equals(this.site)) {
try {
wait();
System.out.println(Thread.currentThread().getName()+":check site is be notifed.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":the site is"+this.site+",I will call user.");
}
}
再一定一个测试类。这次测试类起了四个线程区分别监听里程数变化和地点变化,并且主线程模拟修改里程数。并且通知所有监听的线程:
public class TestClass {
private static Express express = new Express(0,Express.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<2;i++){//三个线程
new CheckSite().start();
}
for(int i=0;i<2;i++){//里程数的变化
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递地点变化
}
}
运行结果:
结果分析:
四个线程分别都进入了有synchronized修饰的同步方法里,说明当使用wait()方法时,该线程会进入阻塞状态并且释放它占有的线程锁。
使用changeKm()中调用notifyAll(),该方法会唤醒所有处在wait()的线程,继续往下执行,一般业务逻辑都会进行再次判断,不满足条件的会继续调用wait(),满足条件的不再执行wait,并且执行相应的逻辑
3.volatile
特点:
1)被volatile修饰的变量保证对所有线程可见,即具有可见性。jvm保证每次变量都从内存中去读,而不是从cpu cache中。
2)被volatile修饰的变量不具有原子性。在并发环境下是不安全的。必须加锁机制。
3)被volatile修饰的变量会禁止重新优化排序。volatile设置的排序会使用内存屏障
volatile适合应用在一写多读的并发环境下。具体场景可参考这个大神查看这位大神的博客
示例代码:
public class VolatileTest extends Thread {
static volatile int increase = 0;
static AtomicInteger aInteger=new AtomicInteger();//对照组
static void increaseFun() {
increase++;
aInteger.incrementAndGet();
}
public void run(){
int i=0;
while (i < 10000) {
increaseFun();
i++;
}
}
public static void main(String[] args) {
VolatileTest vt = new VolatileTest();
int THREAD_NUM = 10;
Thread[] threads = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
threads[i] = new Thread(vt, "线程" + i);
threads[i].start();
}
//idea中会返回主线程和守护线程,如果用Eclipse的话改为1
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("volatile的值: "+increase);
System.out.println("AtomicInteger的值: "+aInteger);
}
}
运行结果:
结果分析:
volatile具有可见性,但按猜想结果也应是100000,但运行时发现会小于100000,与AtomicInteger(原子类,后续会讲,保证是线程安全的)相比是线程不安全的。
4.ThreadLocal
threadLocal各个线程之间的数据独立。原因在于:
1)因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
2)既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
内部实现是用的ThreadLocalMap这样的一个map。
使用场景:
1)每个线程需要有自己单独的实例
2)实例需要在多个方法中共享,但不希望被多线程共享
代码示例:
public class ThreadLocalTest {
//线程局部变量
static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected Integer initialValue() {
return 1;
}
};
/**
* 运行3个线程
*/
public void StartThreadArray(){
Thread[] runs = new Thread[3];
for(int i=0;i
运行结果:
结果分析:
有三个线程同时操作同一theadLocal变量,从运行结果中可以看出,该变量在各个线程中是相互独立的。
上述只是对各个关键字或者用法作了描述,并没有做深一步的原理探究,之后会单独对每个点的原理做下探究和分析