前言
那我在搜索多线程有关于锁的知识的时候发现,基本上所有的博客内容都是出自同一篇文章,啊,知识就是这样传播开来的。那我就觉得我既然也学习了这个知识点那就做点不一样的,当然我也很希望大家都能转载我这篇博客,留言说一声就行,点个赞再走也不错,能收藏那就是极好不过了,不转载学习讨论留言评论区大家一起探讨也是极好的。 好,开始我们的学习,那在开始本次的学习中我希望大家能够了解,什么是锁,锁的定义概念是什么,锁它要什么用,锁它会在哪里去使用呢? 其实我们生活中到处都有锁的身影,自行车锁,车锁,门锁,还有你们那难以打开的心锁。。。等等。对吧,那我们都知道锁的存在,那谁能说一下锁它为什么要存在呢?好,没人说,那我说啊,那不管是上面的哪个锁是不是都是在防止盗窃,防止我们的财产不被他人恶意侵占,防止不好的事情发生。那锁的存在就给了我们一定程度上的安全感没错吧。那这也是Java中锁的意义所在。那我们的显式锁与隐式锁的意义也就是在Java多线程的任务执行中保护我们线程能够好好的,不被阻断的进行下去。
那谈到了锁就必然会存在安全问题。我们也经常听到别人说线程安全不安全的,注意,面试官也经常问线程安全的奥。 那既然涉及到了这个知识点,那我们先来聊一聊什么是线程安全。
线程安全
什么是线程安全?
多个线程同一时刻对同一个全局变量(同一份资源)做写操作(读操作不会涉及线程安全)时,如果跟我们预期的结果一样,我们就称之为线程安全,反之,线程不安全。 不知道大家打过架没有?反正我肯定是没有打过架了,好好学生一枚的说!(我都是往死里打。),好继续说回来。那假设你们家你有个姐姐,没错,总是欺负你的那个女人,她每次都会在你们吃饭的时候和你抢饭吃,不是穷,没饭吃,而是总抢你的饭吃。你没有听错,她就是总抢你的饭吃。(那时候我还小,打不过她,到了10+了,更打不过了,女孩发育早的说。别给我说,什么姐姐最好了,那我小的时候最怕的就是她,很烦,打也打不过,你放弃,她还穷追不舍,拿话讽刺你。啊,那种感觉我现在回想起来还是有点气。)。好,回归到打架的阶段,那她总是要和你吃同一口,抢一个食,那是不是你们就筷子会打架,最后你被打。对吧,那这就是不安全。
源码案例解释
public class Demo7 {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count=10;
@Override
public void run() {
while (count>0){
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票:"+count);
}
}
}
}
没错,就是市面上的例子,我就不浪费心思了,反正知识点都是一样的。 好,我们来瞧一瞧在线程不安全下的运行结果。  通过上面的测试结果,三个线程,同时抢票,有时候会抢到同一张票?那在这里我先说一个情况,我这次的线程运行中没有打印出余票为负数的情况的,但是这种情况也是存在的,因为在第一个线程出票为0之后,那第二个第三个线程还在继续执行,也就出现了余票为-1,-2的现象。那我们为了能够更好地理解这个卖票的过程,我们用画图来讲一下。
 注意,重点来了 那假设我们的票count卖着卖着卖的就剩一张了,那A继续到while判断,count=1,是大于0的,那继续往下,当走到我们的try-catch的时候就会耗费一些时间,当然,sleep方法会消耗时间(sleep方法是放大了犯错的几率),但是即使没有,也有可能因为时间偏的丢失,导致B抢到了,那这个时候B也进行判断,往下执行,之后C也执行了,也就是说,我们的三个线程,同时处在任务中,那A走完打印是不是余票0,之后B再打印那就是-1,C就是-2了,这也会死为什么有可能出现-2的原因。 好,这里引入一个知识点:
时间偏指的是CPU分出来的一个个的时间。即抢到的时间的概率越大。
这里就又涉及到我们的线程调度问题,那这里就不再多说了,想知道的可以自主查找或者留言评论。那我们其中的抢占调度就是我们Java使用的调度机制,当CPU空闲以后,CPU会主动抛出时间偏,由各个线程争抢,抢到了就去执行。 那通过以上的案例也让我们更好的理解到线程安全,那怎么解决我们的线程安全问题就是这次博客的真正重点了。
解决线程安全问题方法
1、隐式锁:同步代码块
被标记的锁对象,Java中任何对象都可以传进来,任何对象都可以打标记
public class Demo8 {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count=10;
private Object o= new Object();
@Override
public void run() {
while (count>0){
synchronized (o){
if (count>0){
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count+"张"+",时间:"+System.currentTimeMillis());
}else {
break;
}
}
}
}
}
}
为了让大家能够直观的感受到打印的时间间隔,我们测试运行结果如下:  通过图片我们可以发现,同一时间,抢票的间隔差不多都是1000ms,为什么,不是说多线程吗? 因为在抢票的方法上,增加了synchronized,导致同一时候,只能有一个线程运行,需要等这个线程运行完后,下一个线程才能运行。 大家可以认为我们去优衣库买衣服,那优衣库是不是都有试衣间对吧。那假设试衣间有两个,但是只有一个门,也就是张三进去试衣服了,把门关上了,那是不是两个试衣间都不能被使用了,只有张三出来才能供一个人使用。
2、隐式锁:同步方法
public class Demo9 {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count=10;
@Override
public void run() {
while (true){
boolean flag = sale();
if (!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count>0) {
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count+"张"+",时间:"+System.currentTimeMillis());
return true;
}
return false;
}
}
}
以方法为单位进行加锁。在方法名称前面加上修饰符synchronized,它的锁是this,调用该方法的对象就是锁,但当方法被静态修饰的时候锁对象就是类名.class。而创建(new)了多个对象以后,每个线程执行的对象互不相同,就不会由排队的情况,因为它们的锁不一样,给判断各自的锁。 测试结果是一样的,就不再多发图了,这里只是向大家展示两种不同方式。
3、显式锁:Lock
API的链接:最新版API JDK11版本中文释义 Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁 首先我们来看一下Lock的API:  看完就一个感受,灵活啊,这也太活了,想锁就锁,想开就开,这不是任意妄为?但是CSDN也不是法外之地,所以不要太活。 那在我们进行源码操作之前,我们先来看一下它的构造方法。  那我们其实主要就是进行上锁与解锁。,剩下的呢就由大家自行探索了。 上锁的介绍: 解锁的介绍:  好,上源码:
public class Demo10 {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count=10;
private Lock l = new ReentrantLock();
@Override
public void run() {
while (true){
l.lock();
if (count>0){
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票:"+count);
}else {
break;
}
l.unlock();
}
}
}
}
测试结果是一样的: 
总结
那其实从开始到现在了,想必大家对二者的区别也是有了一定的了解。
-
所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。 那其实对于手动操作的lock锁我更偏爱,因为它可以随意指定位置,我们在之前的API中也可以看出,我们甚至可以在执行的进程中去上锁中断它,就是这么无敌。 -
这也是为什么,在各博客说的等待是否可中断? 1、synchronized是不可中断的。除非抛出异常或者正常运行完成 。 2、 Lock可以中断的。中断方式: 1. :调用设置超时方法tryLock(long timeout ,timeUnit unit)
2. :调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
-
加锁的时候是否可以公平? synchronized:非公平锁 lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。 true:公平锁 false:非公平锁 -
从性能分析 synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。 但是我现在还是建议使用lock,未来是未来,现在那就使最优的啊,但是我们也要对synchronized有所了解就行了,到了用的时候咱也不怵。
好了,在观看了大量的精品复制博客后,我也摘抄了不少,但是学习一定要有自己的思考,这是我想说的。
|