锁的定义及如何使用

Java中的锁是什么?

在并发编程中,经常会遇到多个线程访问同一个共享变量,当同时对共享变量进行读写操作时,就会产生数据不一致的情况。

为了解决这个问题

  • JDK 1.5 之前,使用 synchronized 关键字,拿到 Java 对象的锁,保护锁定的代码块。JVM 保证同一时刻只有一个线程可以拿到这个 Java 对象的锁,执行对应的代码块。
  • JDK 1.5 开始,引入了并发工具包 java.util.concurrent.locks.Lock,让锁的功能更加丰富。

常见的锁

  • synchronized 关键字锁定代码库
  • 可重入锁 java.util.concurrent.lock.ReentrantLock
  • 可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock

Java 中不同维度的锁分类

可重入锁

  • 指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。JDK 中基本都是可重入锁,避免死锁的发生。上面提到的常见的锁都是可重入锁。

公平锁 / 非公平锁

  • 公平锁,指多个线程按照申请锁的顺序来获取锁。如 java.util.concurrent.lock.ReentrantLock.FairSync
  • 非公平锁,指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程先获得锁。如 synchronized、java.util.concurrent.lock.ReentrantLock.NonfairSync

独享锁 / 共享锁

  • 独享锁,指锁一次只能被一个线程所持有。synchronized、java.util.concurrent.locks.ReentrantLock 都是独享锁
  • 共享锁,指锁可被多个线程所持有。ReadWriteLock 返回的 ReadLock 就是共享锁

悲观锁 / 乐观锁

  • 悲观锁,一律会对代码块进行加锁,如 synchronized、java.util.concurrent.locks.ReentrantLock
  • 乐观锁,默认不会进行并发修改,通常采用 CAS 算法不断尝试更新
  • 悲观锁适合写操作较多的场景,乐观锁适合读操作较多的场景

粗粒度锁 / 细粒度锁

  • 粗粒度锁,就是把执行的代码块都锁定
  • 细粒度锁,就是锁住尽可能小的代码块,java.util.concurrent.ConcurrentHashMap 中的分段锁就是一种细粒度锁
  • 粗粒度锁和细粒度锁是相对的,没有什么标准


偏向锁 / 轻量级锁 / 重量级锁

  • JDK 1.5 之后新增锁的升级机制,提升性能。
  • 通过 synchronized 加锁后,一段同步代码一直被同一个线程所访问,那么该线程获取的就是偏向锁
  • 偏向锁被一个其他线程访问时,Java 对象的偏向锁就会升级为轻量级锁
  • 再有其他线程会以***自旋的形式尝试获取锁***,不会阻塞,自旋一定次数仍然未获取到锁,就会膨胀为重量级锁

自旋锁

  • 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环占有、浪费 CPU 资源

锁如何使用?有什么注意事项?

Java 中常见的锁有

  • synchronized
  • 可重入锁 java.util.concurrent.lock.ReentrantLock
  • 可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock

synchronized 有 3种用法

  • 修饰普通方法,执行方法代码,需要获取对象本身 this 的锁
package constxiong.concurrency.a18;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试 synchronized 普通方法
 * @author ConstXiong
 * @date 2019-09-19 10:49:46
 */
public class TestSynchronizedNormalMethod {

	private int count = 0;
	
//	private void add1000() {
	private synchronized void add1000() { 
        //使用 synchronized 修饰 add100 方法,即可获得正确的值 30000
		for (int i = 0; i <1000; i++) {
			count++;
		}
	}
	
	//启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
	private void test() throws InterruptedException {
		List<Thread> threads = new ArrayList<Thread>(10);
		
		for (int i = 0; i <30; i++) {
			Thread t =  new Thread(() -> {
				add1000();
			});
			t.start();
			threads.add(t);
		}
		
		//等待所有线程执行完毕
		for (Thread t : threads) {
			t.join();
		}
		
		//打印 count 的值
		System.out.println(count);
	}
	
	public static void main(String[] args) throws InterruptedException {
		//创建 TestSynchronizedNormalMethod 对象,调用 test 方法
		new TestSynchronizedNormalMethod().test();
	}
}
  • 修饰静态方法,执行方法代码,需要获取 class 对象的锁
package constxiong.concurrency.a18;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试 synchronized 静态方法
 * @author ConstXiong
 * @date 2019-09-19 10:49:46
 */
public class TestSynchronizedStaticMethod {

	private static int count = 0;
	
	private static void add1000() {
//	private synchronized static void add1000() { //使用 synchronized 修饰 add100 方法,即可获得正确的值 30000
		for (int i = 0; i <1000; i++) {
			count++;
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		//启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
		List<Thread> threads = new ArrayList<Thread>(10);
		
		for (int i = 0; i <30; i++) {
			Thread t =  new Thread(() -> {
				add1000();
			});
			t.start();
			threads.add(t);
		}
		
		//等待所有线程执行完毕
		for (Thread t : threads) {
			t.join();
		}
		
		//打印 count 的值
		System.out.println(count);
	}
}
  • 锁定 Java 对象,修饰代码块,显示指定需要获取的 Java 对象锁
package constxiong.concurrency.a18;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试 synchronized 代码块
 * @author ConstXiong
 * @date 2019-09-19 10:49:46
 */
public class TestSynchronizedCodeBlock {

	private int count = 0;
	
	//锁定的对象
	private final Object obj = new Object();
	
	private void add1000() {
		
		//执行下面的加 1000 的操作,都需要获取 obj 这个对象的锁
		synchronized (obj) {
			for (int i = 0; i <1000; i++) {
				count++;
			}
		}
	}
	
	//启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
	private void test() throws InterruptedException {
		List<Thread> threads = new ArrayList<Thread>(10);
		
		for (int i = 0; i <30; i++) {
			Thread t =  new Thread(() -> {
				add1000();
			});
			t.start();
			threads.add(t);
		}
		
		//等待所有线程执行完毕
		for (Thread t : threads) {
			t.join();
		}
		
		//打印 count 的值
		System.out.println(count);
	}
	
	public static void main(String[] args) throws InterruptedException {
		//创建 TestSynchronizedNormalMethod 对象,调用 test 方法
		new TestSynchronizedCodeBlock().test();
	}
}

可重入锁 java.util.concurrent.lock.ReentrantLock 的使用示例

package constxiong.concurrency.a18;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 测试 ReentrantLock 
 * @author ConstXiong
 * @date 2019-09-19 11:26:50
 */
public class TestReentrantLock {

	private int count = 0;
	
	private final Lock lock = new ReentrantLock();
	
	private void add1000() {
		lock.lock();
		try {
			for (int i = 0; i <1000; i++) {
				count++;
			}
		} finally {
			lock.unlock();
		}
	}
	
	//启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
	private void test() throws InterruptedException {
		List<Thread> threads = new ArrayList<Thread>(10);
		
		for (int i = 0; i <30; i++) {
			Thread t =  new Thread(() -> {
				add1000();
			});
			t.start();
			threads.add(t);
		}
		
		//等待所有线程执行完毕
		for (Thread t : threads) {
			t.join();
		}
		
		//打印 count 的值
		System.out.println(count);
	}
	
	public static void main(String[] args) throws InterruptedException {
		//创建 TestReentrantLock 对象,调用 test 方法
		new TestReentrantLock().test();
	}
	
}

可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock 的使用示例

package constxiong.concurrency.a18;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * 测试可重入读写锁 ReentrantReadWriteLock
 * @author ConstXiong
 * @date 2019-09-19 11:36:19
 */
public class TestReentrantReadWriteLock {
	
	//存储 key value 的 map
	private Map<String, Object> map = new HashMap<String, Object>();
	
	//读写锁
	private final ReadWriteLock lock = new ReentrantReadWriteLock();
	
	/**
	 * 根据 key 获取 value
	 * @param key
	 */
	public Object get(String key) {
		Object value = null;
		lock.readLock().lock();
		try {
			Thread.sleep(50L);
			value = map.get(key);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.readLock().unlock();
		}
		return value; 
	}
	
	/**
	 * 设置key-value
	 * @param key
	 */
	public void set(String key, Object value) {
		lock.writeLock().lock();
		try {
			Thread.sleep(50L);
			map.put(key, value);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.writeLock().unlock();
		}
	}
 
	//测试5个线程读数据,5个线程写数据
	public static void main(String[] args) {
		//创建测试可重入读写锁 TestReentrantReadWriteLock 对象
		TestReentrantReadWriteLock test = new TestReentrantReadWriteLock();
		
		String key = "lock";//存入 map 中的 key
		Random r = new Random();//生成随机数作为 value
		
		for (int i = 0; i <5; i++) {
			//5 个线程读 map 中 key 的 value
			new Thread(() -> {
				for (int j = 0; j <10; j++) {
					System.out.println(Thread.currentThread().getName() + " read value=" + test.get(key));
				}
			}).start();
			
			//5 个线程写 map 中 key 的 value
			new Thread(() -> {
				for (int j = 0; j <10; j++) {
					int value = r.nextInt(1000);
					test.set(key, value);
					System.out.println(Thread.currentThread().getName() + " write value=" + value);
				}
			}).start();
		}
	}
	
}

锁的使用注意事项

  • synchronized 修饰代码块时,最好不要锁定基本类型的包装类,如 jvm 会缓存 -128 ~ 127 Integer 对象,每次向如下方式定义 Integer 对象,会获得同一个 Integer,如果不同地方锁定,可能会导致诡异的性能问题或者死锁
Integer i = 100; 
  • synchronized 修饰代码块时,要线程互斥地执行代码块,需要确保锁定的是同一个对象,这点往往在实际编程中会被忽视
  • synchronized 不支持尝试获取锁、锁超时和公平锁
  • ReentrantLock 一定要记得在 finally{} 语句块中调用 unlock() 方法释放锁,不然可能导致死锁
  • ReentrantLock 在并发量很高的情况,由于自旋很消耗 CPU 资源
  • ReentrantReadWriteLock 适合对共享资源写操作很少,读操作频繁的场景;可以从写锁降级到读锁,无法从读锁升级到写锁
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值