浅谈synchronized和lock
1.1为什么要加锁?
多线程情况下,同时操作同一个共享资源的时候,可能会出现业务安全问题。这时候就需要给共享资源上把锁,只能一个进去,操作完后,再让下一个进去,防止共同操作出现问题。
1.2加锁的三种方法
同步代码块
同步方法
lock锁
1.2.1同步代码块
格式:
synchronized (//对象){
//访问共享资源的核心代码
}
可以锁一个list等引用类型数据,但必须是同一个对象,否则无意义。
对于实例方法建议使用this作为锁对象
对于静态方法建议使用类名.class 对象作为锁对象
不推荐锁字符串
synchronized ("字符串"){
//访问共享资源的核心代码
}
这样是没有任何意义,字符串不可变的且在字符串常量池中共享,那么相当于没有加锁。
用同步代码块做案例(看完1.2.2.2的案例再回看)
public class SynBlockTest {
public static void main(String[] args) {
// 1.定义抽奖池
List<Integer> list = new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300);
// 2.定义线程类,可以接收线程名称和集合作为参数
new MyThread("抽奖箱1",list).start();
new MyThread("抽奖箱2",list).start();
}
}
class MyThread extends Thread{
private List<Integer> list;
public MyThread(String name, List<Integer> list){
super(name);
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
/*不加这个,list集合删除完了,第二线程进入list.get 会出错*/
if (list.size() == 0) {
break;
}
Random r = new Random();
int num = r.nextInt(list.size());
System.out.println(Thread.currentThread().getName() + "又产生了一个" + list.get(num) + "元大奖");
list.remove(num);
}
}
}
}
锁代码块锁的是list,在代码中list只有一份,这样操作可以实现只让一个线程进去,另一个线程等待。
1.2.2同步方法
格式:
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
1.2.2.1一个简单的案例
需求:
1.有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池用一个集合表示,集合存储了10,5,20,50,100,200,500,800,2,80,300了这些数据。
2.创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”,随机从集合中获取奖项元素(要求已经抽取过的不能再抽取)直到抽完为止并打印在控制台上
public class SynMethonTest {
static ArrayList<Integer> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
//抽奖池
Collections.addAll(list, new Integer[]{10, 5, 20,50,100,200,500,800,2,80,300});
//创建两个线程
Thread2 thread1 = new Thread2("抽奖箱1", list);
Thread2 thread2 = new Thread2("抽奖箱2", list);
thread1.start();
thread2.start();
}
}
class Thread2 extends Thread{
private List list;
//取名字,赋值list
public Thread2(String name, List list) {
super(name);
this.list=list;
}
@Override
public void run() {
//执行线程方法
while (list.size()>0) {
try {
remove();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
synchronized void remove() throws InterruptedException {
Random r = new Random();
String name=Thread.currentThread().getName();
if (list.isEmpty()){
return;
}
int num = r.nextInt(list.size());
System.out.println(name +"又产生了一个"+list.get(num)+"元大奖");
list.remove(num);
}
}
可以看看这写的是否有问题
答案是有问题。
运行后会发现索引出错
Exception in thread "抽奖箱2" java.lang.IndexOutOfBoundsException: Index 8 out of bounds
那为什么会出现这个问题呢?不是给方法添加了锁了吗?
这时候可以回顾关于锁的知识。要锁同一个对象,这里锁方法,也就是锁实例方法,然后main方法中创建了两个Thread2线程对象,两个对象中运行两个实例方法,两个实例方法各不影响,所以 synchronized void remove() 方法并没有想象那样只有一个线程进来,另一个线程在锁方法外等待,而是能一起进去。
1.2.2.2debug多线程
断点main方法,右键断点,选择Thread,选择Done
断点锁方法,重复操作
执行完main方法
选择线程一
操作线程一进入方法,然后切换线程2
可以看到线程2也可以进入锁方法
这里案例一开始为什么会出现索引越界就很明朗了,两个线程都能进入这个锁方法,并没有锁的效果。
1.2.2.3案例的代码的修改
public class SynMethonTest {
/*如果要main现在用到类变量,要把这类变量变成静态的*/
static ArrayList<Integer> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
//抽奖池
Collections.addAll(list, new Integer[]{10, 5, 20,50,100,200,500,800,2,80,300});
//创建两个线程
Thread2 thread1 = new Thread2("抽奖箱1", list);
Thread2 thread2 = new Thread2("抽奖箱2", list);
thread1.start();
thread2.start();
}
}
class Thread2 extends Thread{
static private List list;
//取名字,赋值list
public Thread2(String name, List list) {
super(name);
this.list=list;
}
@Override
public void run() {
//执行线程方法
while (list.size()>0) {
try {
//不同类中怎么使用其他类的方法,只能静态、创对象?
remove();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
static synchronized void remove() throws InterruptedException {
Random r = new Random();
String name=Thread.currentThread().getName();
if (list.isEmpty()){
return;
}
int num = r.nextInt(list.size());
System.out.println(name +"又产生了一个"+list.get(num)+"元大奖");
list.remove(num);
}
}
这里将线程内的remove 方法添加锁,同时声明为静态方法。静态方法在内存只有一份,则就满足操作同一个对象,锁就生效了。
1.2.3Lock锁
格式:
// 创建Lock对象
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
try {
.....
} finally {
lock.unlock(); // 解锁
}
用完一定释放锁,同try/catch/fianlly 方式保证能最后释放锁
上面案例用lock锁的做法
public class BlockTest{
public static void main(String[] args) {
// 抽奖池
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300);
// 创建Lock对象
Lock lock = new ReentrantLock();
// 创建两个线程,传入同一个List和Lock对象
Thread1 thread1 = new Thread1("抽奖箱1", list, lock);
Thread1 thread2 = new Thread1("抽奖箱2", list, lock);
thread1.start();
thread2.start();
}
}
class Thread1 extends Thread {
private final String name;
private final List<Integer> list;
private final Lock lock;
public Thread1(String name, List<Integer> list, Lock lock) {
super(name);
this.name = name;
this.list = list;
this.lock = lock;
}
@Override
public void run() {
// 执行线程方法
while (true) {
lock.lock(); // 加锁
try {
if (list.size() > 0) {
Random r = new Random();
int num = r.nextInt(list.size());
System.out.println(name + "又产生了一个" + list.get(num) + "元大奖");
list.remove(num);
} else {
break;
}
} finally {
lock.unlock(); // 解锁
}
}
}
}
可发现,一样是产生两个实例,用lock锁并不会出现锁方法的问题。
线程一操作进入后,切换线程二无法进入
1.3总结
如果能用锁代码块就代码块,如果考虑锁方法要考虑是否只有一份对象的问题。使用 synchronized 关键字修饰的同步方法是以实例为单位进行同步的,每个实例都有自己的锁。不同的实例之间互相不影响。而使用 lock 时,可以明确地创建一个锁对象,并且多个实例之间可以共享同一个锁对象,所以可以更加灵活地控制线程的同步。
使用lock锁必须要释放锁,用try/catch/finally 强制最后释放锁,防止死锁。