并发问题根本原因是线程执行程序的过程中并不是线性连续的,一个线程执行完才换另一个,可以第一个线程执行一半,第二个线程就开始执行了。之前的blog关于多线程,并发,临界区有过描述。一个可能被多个线程访问而产生并发问题的变量就是临界变量。


Java实现并发的主要关键字是synchronized,这个关键字相当于一把锁,它用来锁住一个对象,如果线程要执行带有synchronized的方法或者代码块,一定要先获得这把锁。所以synchronized锁住的不是一段代码,而是一个对象,只不过要执行这段代码,一定要现获得这个对象的锁。所以锁是在堆空间实现,synchronized在常规方法中是锁定this[堆内存],静态方法中是锁定class[静态区,也是堆内存]

public class T {
	private int count = 10;
	private Object o = new Object();
	
	public void m() {
		synchronized(o) {
			count--;
			System.out.println(Thread.currentThread().getName()+ " count = " + count);
		}
	}
}

这里就先new object出来,用于加锁。但是每次都要消耗内存,单纯用于加锁,过于浪费,所以反正只是用来加锁的对象,可以直接使用this关键字,表示用当前对象加锁。


public class T {
	private int count = 10;
	
	public void m() {
		synchronized(this) {
			count--;
			System.out.println(Thread.currentThread().getName()+ " count = " +count);
		}
	}
}

上面的代码中m()方法,其实从方法开始就加锁,结束时才释放锁,这种情况下有更简单也更常见的写法,用synchronized关键字直接修饰方法,这两种写法等价。所以也侧面说明synchronized用于对对象加锁而不是代码,而是执行代码的时候锁定当前对象。

public class T {
	private int count = 10;
	
	public synchronized void m() {
		count--;
		System.out.println(Thread.currentThread().getName()+ " count = " +count);
	}
}

通过实现runnable接口也可以实现多线程,但是如果下面的run方法没有关键字synchronized,count的值就可能出现重复,这是因为出现了脏读。加上关键字就不会出现这个问题,因为synchronized关键字的代码块相当于原子操作,不会因为多线称就把代码的执行分开。比如,不会出现线程A执行run方法的过程中,线程B也开始执行。

public class T implements Runnable{
	private int count = 10;
	
	public synchronized void run() {
		count--;
		System.out.println(Thread.currentThread().getName() + " count = " + count);
	}
	
	public static void main(String[] args) {
		T t = new T();
		for(int i = 0; i < 5; ++i) {
			new Thread(t, "THREAD" + i).start();
		}
	}
}

如果一个synchronized修饰的方法m1和一个一般方法m2是可以同时执行的,m2并不需要等m1释放锁才能执行,因为m2并不需要检查锁就可以直接执行。这就好比m1进了拿着1号房间的钥匙开锁进入1号房间,m2进入了没有锁的2号房间,二者并不冲突。

下面一段代码的getBalance()没有synchronized会产生脏读。

public class Account {
	String name;
	double balance;
	
	public synchronized void set(String name, double balance) {
		this.name = name;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.balance = balance;
	}
	
	public double getBalance() {
		return this.balance;
	}

	public static void main(String[] args) {
		Account a = new Account();
		new Thread(()->a.set("Tom", 100.0)).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(a.getBalance());
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(a.getBalance());
	}
}

一个同步方法可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,也就是说synchronized获得的锁是可以重入的[获得之后还可以再获得一遍]

public class T{
	public synchronized void m1() {
		System.out.println("m1");
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		m2();
	}
	
	public void m2() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("m2");
	}
	
	public static void main(String[] args) {
		T t = new T();
		t.m1();
	}
}

子类同步方法也可以调用父类的同步方法。下面的代码,子类和父类都是锁定TT对象,因为默认是synchronized(this)

public class T{
	public synchronized void m() {
		System.out.println("m start");
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("m end");
	}
	
	public static void main(String[] args) {
		new TT().m();
	}
}

class TT extends T {
	@Override
	public synchronized void m() {
		System.out.println("child m start");
		super.m();
		System.out.println("child m end");
	}
}

如果线程出现异常,则获得的对象锁自动被释放。下面代码中,1/0计算出异样后,t2会获得锁运行,如果不希望释放锁,则需要加try catch捕获异常并进行相应的操作。

public class T{
	int count = 0;
	
	public synchronized void m() {
		System.out.println(Thread.currentThread().getName() + " start");
		while(true) {
			count++;
			System.out.println(Thread.currentThread().getName() + " count = " + count);
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if(count == 5) {
				int i = 1/0;
			}
		}
	}
	
	public static void main(String[] args) {
		T t = new T();
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				t.m();
			}
		};
		new Thread(runnable,"t1").start();
		try{
			TimeUnit.SECONDS.sleep(3);
		} catch (Exception e) {
			e.printStackTrace();
		}
		new Thread(runnable, "t2").start();
	}
}

volatile关键字主要功能是使变量在线程之间可见,虽然现象是这样但其实背后的原理并不是,参考下面代码:

public class T{
	volatile boolean running = true;
	
	public void m() {
		System.out.println("m start");
		while(running) {
			
		}
		System.out.println("m end!");
	}
	
	public static void main(String[] args) {
		T t = new T();
		new Thread(t::m, "t1").start();
		
		try{
			TimeUnit.SECONDS.sleep(3);
		} catch (Exception e) {
			e.printStackTrace();
		}
		t.running = false;
	}
}

原理是JMM[Java Memory Model],每个线程在运行的时候有内存相对独立,也就是说有自己的一部分内存或者缓冲存,在没有volatile修饰的时候running是一个常规变量,程序运行的时候主线程的内存中会分配一个running变量的内存,t1线程开始执行的时候会把这个变量copy到自己的内存中,这样不用每次需要都去主内存访问,但是此时t2线程修改了主内存的running值,但是t1在持续运行的时候并不知道持续的在读取自己内存中的running值[如果线程t1有空闲,有可能去主内存更新值]。加了volatile并不是让t1内存中的变量可以被其他线程修改,而是在主内存的对应变量被修改后,通知其他线程,你的内存中的变量已经过期,需要再次访问主内存获取新的值,这样对应线程的变量就会获得更新。

能用volatile,就不用synchronized可以提高线程并发度。volatile算是无锁同步,会强制线程去堆内存中更新值,但是volatile可以保证线程之间变量可见,但是不能保证这个变量的原子性。synchronized可以保证可见性和原子性但是效率差一些,Java1.8之后也进行了部分修改。


如果程序对于变量的需求只是简单的加减,可以使用AtomicInteger获得其他Atomic的类型。这是Java自带的实现原子性的类型,因为通过底层实现,效率比synchronized高很多,在注重性能的场景下,应尽量使用Atomic类型。下面的代码可以达到和使用synchronized相同的结果,但速度快很多。

public class T{
	AtomicInteger count = new AtomicInteger(0);
	
	public void m() {
		for(int i = 0; i < 10000; ++i) count.incrementAndGet();
	}
	
	public static void main(String[] args) {
		T t = new T();
		List<Thread> threads = new ArrayList<Thread>();
		for(int j = 0; j < 10; ++j) {
			threads.add(new Thread(t::m, "thread-"+j));
		}
		threads.forEach((o)->o.start());
		threads.forEach((o)->{
			try {
				o.join();
			}catch (Exception e) {
				e.printStackTrace();
			}
		});
		System.out.println(t.count);
	}
}

之前说过synchronized锁住的不是代码,是对象。所以只要锁的对象不变就可以持续防止其他线程执行同步代码,但是一旦锁的对象被修改,那么新的对象没有被加锁,则其他线程就可以加入执行。下面的代码,t1执行的时候,t2也会执行,因为对象o被修改。

public class T{
	Object o = new Object();
	
	public void m() {
		synchronized (o) {
			while(true) {
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
			}
		}
	}
	
	public static void main(String[] args) {
		T t = new T();
		new Thread(t::m, "t1").start();
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		Thread t2 = new Thread(t::m,"t2");
		t.o = new Object();
		t2.start();
	}
}

不要使用String类型的对象当作锁的对象,因为String类型,值一样的话,会被当作同一个对象。下面的例子中,看似锁定的是两个对象,其实是一个对象。更为麻烦的是,调用第三方库的时候,如果看不到源码,同时使用String对象加锁,而开发者碰巧也使用String对象,那么就会产生异常。

public class T{
	String s1 = "sss";
	String s2 = "sss";
	
	void m1() {
		synchronized (s1) {
		}
	}
	
	void m2() {
		synchronized (s2) {
		}
	}
}

wait,notify,notifyAll方法会调用被锁对象的wait,notify和notifAll方法。如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行[无法指定线程]。如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

区别在于,wait会释放锁,而notify和notifyAll不会释放锁。所以如果两个或多个线程同时锁定一个对象,需要通过wait和notify来切换使用权。当notify其他线程时,因为notify方法不会释放锁,需要紧跟wait方法,释放该对象的锁。

尽量避免使用wait/notify,比较复杂且效率不高


CountDownLatch方法有时也可以巧妙的实现多线程访问同一对象时线程切换。CountDownLatch相当于设置一个门闩,具体操作是从设定的值开始往下递减直到0为止,门闩开启。Latch await方法不需要锁定任何对象,等待Latch开启[设置的值变成0]。

因为不需要CountDownLatch不需要锁定任何对象,所以效率相对要高很多。


ReentrantLock是手工锁,必须由开发人员显示地加锁和解锁。可以一定程度上替代synchronized。synchronized遇到异常,jvm会自动释放,但是ReentrantLock需要手动释放,所以在try-catch-finally里释放锁。

同时ReentrantLock提供trylock方法,进行尝试加锁,并且根据结果进行后续逻辑。

ReentrantLock还可以指定为公平锁[new ReentrantLock(true)就是new一个公平锁],首先默认的synchronized都是竞争锁[非公平锁]。比如,一个线程释放锁之后,剩下等待的线程哪个可以获得锁不一定基本是竞争机制[这样做效率高,不需要额外的信息去决定哪个线程获得锁],但是公平锁的机制是等待最久的线程优先获得这个锁

利用ReentrantLock的Condition可以实现类似wait/notify的等待/通知模型[不用使用synchronized]。

所以ReentrantLock更加灵活。


ThreadLocal的用法就是自己线程的变量自己使用,其他线程无法访问。相当于每个变量每个线程都深copy了一份。