按照宇哥所讲及于csdn上的查询自学,先写一个总体的代码,然后去修改验证来得出我们对这个锁的理解。先写一个task类,类中只有num属性,然后我们开五个线程去,每个线程跑一个for循环10000次的代码,看看最后的是不是50000从而完成验证
class Task {
int num;
}
public class test {
public static void main(String[] args) {
test t = new test();
Task ta = new Task();
// 产生五个线程
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() { // 这是一种匿名内部类开启线程的新的方式
@Override
public void run() {
t.test(ta);
}
}
).start();
}
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ta.num);
}
// 创建一个同步锁 锁住的方法
public synchronized void test(Task obj) {
for (int j = 0; j < 10000; j++) {
obj.num++;
}
}
}
/*
上述代码运行结果:50000
*/
由结果可知没问题,但是不知道这里的线程安全是如何实现的,因为有两种方法。所以我们修改一下代码来验证一下。
假如我把for循环换到上面去,调用test方法两万次。
// 这里我只修改一部分 可以对照上面代码可以知道修改了哪里
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
t.test(ta);
}
}
}
).start();
}
// 创建一个同步锁 锁住的方法
public synchronized void test(Task obj) {
obj.num++;
}
/*
运行结果还是50000
*/
我们继续,再创建一个Testa,然后写一个线程ThreadA,来实现TestA的test方法,还是处理原来的Task中的num属性。
class Task {
int num;
}
class TestA {
public synchronized void test(Task obj) {
obj.num++;
}
}
class ThreadA extends Thread {
TestA testA;
Task task;
// 初始化
public ThreadA(TestA testA, Task task) {
this.task = task;
this.testA = testA;
}
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 20000; i++) {
testA.test(task);
}
}
}
public class test {
public static void main(String[] args) {
test t = new test();
Task ta = new Task();
TestA testA = new TestA();
// 产生五个线程
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
t.test(ta);
}
}
).start();
new ThreadA(testA, ta).start(); // 开启ThreadA线程
}
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ta.num);// 输出num值
}
/*
输出结果为:46372
注:这里是输出随机数,但是会小于50,000
*/
那我们再验证一下用suchronized的代码块锁住obj看结果。
public class test {
public void test(Task obj) {
synchronized (obj) {
obj.num++;
}
}
}
class TestA{
// 这里加了synchronized还是不会等于十个线程的num数
public void test(Task obj) {
synchronized (obj) {
obj.num++;
}
}
}
/*
运行结果为:50000
*/
所以由上所述,锁住的并不是形参,而是各自该类所产生的五个线程对象。
所以说加锁应该是指给这个调用该方法的对象加锁!
那现在问题是,锁住调用这个方法的对象有什么作用呢?不能没效果吧
由题可知,锁住对象就把这个对象的所有属性全都锁住了。
class Task {
int num;
}
public class test {
public static void main(String[] args) {
// 这里新增加一个test类对象 t1
test t = new test();
test t1 = new test();
Task ta = new Task();
// 产生五个线程
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() { // 创建线程
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
t.test(ta);
}
}
}
).start();
new Thread(new Runnable() { // 创建线程
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
t1.test(ta);
}
}
}
).start();
}
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ta.num);
}
public synchronized void test(Task obj) {
obj.num++;
}
}
/*
运行结果为:32320
*/
这里为啥又不行了,不是说我们得出的结果是不对的,把之前的理论全都否认掉,而是说这两个对象不是同一个对象,一个是t,一个是t1。所以还是共享数据,所以如何解决这个问题呢,在test基础上加static使其成为静态方法,不管是类还是对象调用,都被锁住了。
}
public class test {
public static synchronized void test(Task obj) {
obj.num++;
}
}
/*
运行结果为:50000
*/
那么LOck与之有异曲同工之处,只不过需要自己手动释放锁
// 上述原始代码的复用,只不过是改了一下
class Task {
int num;
}
public class test {
// 这里加入了static就可以了锁定了调用该方法的类对象或者是类
static Lock lock = new ReentrantLock();
public void test(Task obj) {
lock.lock();
obj.num++;
lock.unlock();
}
}
/*
运行结果为:50000
*/
经过我的学习,我觉得Lock锁的性能比synchronized好,是因为synchronized属于重量级的锁。
synchronized抢占锁的特性
我们先来看一下synchronized抢占锁的特性。synchronized在抢占锁的时候,如果抢占不到,线程直接就进入阻塞状态了,而线程进入阻塞状态,其实什么也干不了,也释放不了线程已经占有的资源,并且也无法主动或者被动打断阻塞获取锁的操作,只有等别的线程释放锁之后才会被唤醒来重新获取锁。
synchronized阻塞获取锁产生的问题
那synchronized这种获取锁阻塞的特性,有什么问题么?其实有一种很重要的问题,那就是会产生死锁的问题。
那什么是死锁?死锁是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。
举个例子来说,线程1先对加A加锁,线程2对B加锁。代码运行到某一时刻,线程1需要对B加锁,但是此时B的锁已经被线程2占有,于是线程1就会阻塞,与此同时线程2同时也需要对A加锁,发现A已经被线程1持有,也会进入阻塞,于是线程1和线程2都在等对方释放资源,就产生了死锁的问题,并且由于synchronized阻塞的特性,线程无法主动或者被动停止阻塞,势必会导致这个死锁永远无法通过主动或者人为干预(其它线程干预)来解决。
那么有什么好的办法来解决阻塞导致死锁的问题呢?
我们分析一下死锁产生的问题主要是线程都在相互等待其它线程释放资源导致的,基于这个问题我们思考一下,如果一个线程获取不到锁,然后就停止获取锁,不阻塞,或者是阻塞一会就不再阻塞,又或是阻塞过程中被其他线程打断,那样这是不是就不是产生死锁的问题了。
就拿上面的例子来说,假设线程1获取B的阻塞锁超过一定时间,主动放弃获取B的锁,那么线程1代码就可以继续往下执行,当执行完之后,线程1释放了A锁,此时线程2就能获取到A的锁,那么线程2就可以继续执行了,这样是不是死锁的问题就完美解决了。
其实Lock锁就提供了上述提到的几种解决方案的api,接下来我们就来看看Lock锁提供的api。
void lockInterruptibly() throws InterruptedException;
阻塞可以被打断的加锁方法,这是一个被动放弃获取锁的方法。就是说其它线程主动当调用阻塞线程的interrupt方法之后,该阻塞线程就会放弃继续获取锁,然后抛出InterruptedException 异常,所以对于使用方来说,只要捕获这个异常,就能保证线程的代码继续执行了。
boolean tryLock();
这个方法是尝试加锁,加锁失败后就放弃加锁,不会阻塞,直接返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
这个方法相比上面的就是尝试加锁失败后在阻塞的一定时间之后,如果还没有获取到锁,那么就放弃获取锁。
Lock接口的实现有很多,但基本上都是基于Java的AQS的实现来完成的。AQS其实主要是维护了一个锁的状态字段state和一个双向链表。当线程获取锁失败之后,就会加入到双向链表中,然后阻塞或者不阻塞,这得看具体的方法实现。
Lock接口的一个实现ReentrantLock就是基于AQS实现来讲的,这里就不继续展开讲解ReentrantLock的实现原理,如果有感兴趣的同学,可以看一下 一文带你看懂Java中的Lock锁底层AQS到底是如何实现的 这篇文章,文章是基于ReentrantLock来讲解AQS的加锁和释放锁的原理。
总结
好了,到这里其实大家应该知道了,为什么需要Lock锁,因为synchronized获取不到锁的时候会阻塞,并且阻塞不可被打断的特性会导致可能会产生死锁的问题,为了解决这个问题,Java就提供了Lock锁的实现,从主动放弃获取锁或者被动放弃获取锁的方式,解决一直阻塞可能产生的死锁问题。