Java并发编程(四)锁的使用(上)

时间:2021-04-01 17:59:03

锁的作用

  锁是一种线程同步机制,用于实现互斥,当线程占用一个对象锁的时候,其它线程如果也想使用这个对象锁就需要排队。如果不使用对象锁,不同的线程同时操作一个变量的时候,有可能导致错误。让我们做一个测试:

class Entity {
	public int value = 0;
}
class IncreaseThread implements Runnable {
	@Override
	public void run() {
		for(int i=0;i < 100000; i++) {
			AndOneTest.instance.value++;
		}
	}
}
public class AndOneTest {
	public static Entity instance = new Entity();
	public static void main(String[] args) throws InterruptedException {
		ExecutorService exec = Executors.newCachedThreadPool();
		exec.execute(new IncreaseThread());
		exec.execute(new IncreaseThread());
		exec.shutdown();
		Thread.sleep(5000);//等待两个线程执行结束
		System.out.println("Value = " + instance.value);
	}
}

5秒后输出以下结果,如果重新运行程序,得出的结果还会不同:

Value = 111260

我们创建了两个线程,这两个线程同时对AddOneTest.value执行十万次自增操作,我们期望的值是200000,然而得到的结果却并不是。假设两个线程都要对int value=0变量实现value++操作,value++操作会被虚拟机分成三步执行,

1.读取value当前的值。

2.将这个值加1。

3.将加1的结果写入value变量。

两个线程执行的顺序有可能是:

1.线程一读取value值(0)

2.线程二读取value值(0)

3.线程一将值加1(1)

4.线程二将值加1(1)

5.线程一将结果写入value变量(1)

6.线程二将结果写入value变量(1)

最后两个线程执行的结果是i=1,而不是i=2。因此我们无法得到Value =200000。线程之间的运行顺序的可不预测性会导致我们得不到正确的结果,因此我们需要加锁来保证结果是正确的。

 

Java内置锁

内置锁使用synchronized关键字定义,synchronized关键字有两种使用方法,一种是作为修饰词定义在方法中代码如下:

public synchronized void test() {
	//临界区
}

另一种是指定一个对象,后面接一个代码块,代码如下:

public void test() {
     synchronized(object) {
		//临界区
	}
}

二者有两个区别:

1. 锁的对象不同,第一种方式获取的是当前对象的锁,相当于synchronized(this){},第二种方法获取的是指定对象的锁。

2. 作用域不同,第一种方式锁的是整个方法,第二种锁的只是代码块内部。

使用锁对上例自增测试的改进,只需要在AndOneTest.instance.value++外面加上如下代码即可:

synchronized(AndOneTest.instance) {
    AndOneTest.instance.value++;
}

  

运行后5秒后输出以下结果:

Value = 200000

 

这里有个需要注意的地方:1. 所有的锁都对应一个对象,同一个对象锁之间会互斥;2. 不同对象锁之间不会互斥;3. 没有申请锁的方法不和任何对象锁互斥。因此我们需要对哪个对象进行修改的时候就获取哪个对象的锁,这样就可以保证不同的线程不会同时修改这个对象。如果需要对两个对象修改,则应分别获取两个对象的锁,代码如下:

public void change() {
	synchronized(instanceA) {
		//对instanceA进行修改
	}//释放对instanceA的锁
	synchronized(instanceB) {
		//对instanceB进行修改
	}//释放对instanceB的锁
}

当synchronized关键字修饰static方法时,获取的就不是当前对象的锁了,而是类对象锁,因为调用static方法时可能类还没有对象。实际上类锁也有一个对应的对象,它所对应的对象是ClassName.class。这个对象用于存储类信息的,在使用反射的时候经常使用到。

class TargetClass{
	public synchronized static void sleepMethod() {
		try {
			Thread.sleep(3000);//睡3秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("我睡醒了");
	}
	public synchronized static void getLockMethod() {
		synchronized(Target.class) {
			System.out.println("我得到了class锁");
		}
	}
}

class ExampleThread implements Runnable {
	@Override
	public void run() {
		TargetClass.sleepMethod();
	}
}
public class StaticLockTest {
	public static void main(String[] args) throws InterruptedException {
		ExecutorService exec = Executors.newCachedThreadPool();
		exec.execute(new ExampleThread());
		exec.shutdown();
		Thread.sleep(100);//等待新创建的线程获得锁
		TargetClass.getLockMethod();
	}
}

3秒钟之后输出以下结果:

我睡醒了

我得到了class锁

 

执行main()方法的线程我们称之为主线程,此外我们还通过线程池创建了一个新的线程,新线程执行sleepMethod(),主线程执行getLockMethod()。启动新线程后主线程会等待新线程0.1秒,以确保新线程拿到了锁,0.1秒之后,主线程想获得锁,但是锁已经被占用了,只能等到新线程执行完sleepMethod()方法。本例中synchronized static synchronized(TargetClass.class){}等价,获得的都是TargetClass.class对象的锁。

总结

未完待续。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。