Home Tags Posts tagged with "多线程"

多线程

对于大部分程序员来说,如果你学习的技术,并不能转换为生产工具,那就毫无意义。因此,在学习并发编程之前,我们有必要了解

什么是并发(转载)

作者:大宽宽
链接:https://www.zhihu.com/question/307100151/answer/894486042
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

深度好文!!!值得推荐!!!

【并发】(Concurrency)是由【P进程】引申出来的抽象概念。

上面说到了你可以假设自己一个人按照一定的步骤来铺路,一个人从头干到尾,这是一个“串行”的【P进程】。

但你也可以假设有2个人铺路。比如你可以按照长度分两半,一人铺500m * 50m;也可以按宽度划分,一人铺1000m * 25m;你还可以说让一个人负责铺全部路面的前5个步骤,另外一个人负责铺路面的余下5个步骤。然后你可以进一步想,假如不是雇2个人,而是雇20个人概如何分工呢?你可以混搭按长度,宽度,步骤等各种方式进行拆分。你甚至可以考虑这20个人不是完全一样的,有的能力强,有的能力弱,可以适当的调整工作量的比例等等。

不管怎样拆,都意味着你得到了【并发】的【P进程】。换成说人话就是,你有一套方案,可以让多个人一起把事情做的更高效。注意是“可以“让事情更高效,而不是“必然“让事情更高效。是不是更高效要看到底是怎么执行的,后边会讲。

举个写代码的例子,你有一个很长很长的数组,目标是把每一个数都*2。一个并发的做法就是把数组拆为很多个小段,然后每个小段的元素依次自己*2。这样的程序写出来就是一个【并发】的【程序】。这个程序如果运行起来就是【并发】的【OS进程】。

这时就会出现一个问题,当你想把一个【并发】的【P进程】写成程序时,你怎么用编程语言告诉操作系统你的程序的一些步骤是【并发】的。更确切地说,你需要一个写法(可能是语法,也可能是函数库)表达:

  • 几个任务是【并发】的
  • 【并发】的任务之间是怎么交互协作的

为了解决这两个问题,人们总结了一些方法,并将其称为“并发模型”。

比如:

  • Fork & Join模型(大任务拆解为小任务并发的跑,结果再拼起来)
  • Actor模型(干活的步骤之间直接发消息)
  • CSP模型(干活的步骤之间订阅通话的频道来协作)
  • 线程&锁模型(干活的人共享一个小本本,用来协作。注意小本本不能改乱套了,所以得加锁)
  • ……

以Java中的线程为例,大家想表达【并发】就启动新的Thread(或者某种等价操作,如利用线程池);想让Thread之间交互,就要依靠共享内容。但是【并发】的Thread如果同时修改同一份数据就有可能出错(被称为竞争问题),为了解决这个问题就要引入锁(Lock,或者一些高级的同步工具,如CountdownLatch,Semaphore)。

特别强调下,Java的线程是表达并发的概念的类。这个类在绝大部分操作系统上使用操作系统内核中的【线程】实现。二者之间还是有一些细微的差异。即用开发者用Java Thread写代码表达思路,和操作系统调度线程执行是两个层面的事情。请努力认识到这一点。

再比如Erlang是基于Actor的并发模型(其实这是原教旨主义的OO)。那么就是每个参与【并发】的任务称为Process(又一个进程……,和【P进程】以及【OS进程都不太一样】,叫【E进程】好了,Erlang中的”进程“)。【E进程】之间通过消息来协作。每个【E进程】要不是在处理消息,要不就是在等新的消息。

如果你用go,那么表达并发的工具就是goroutine,goroutine之间协作要用channel。(当然也可以用Sync包加锁,不展开)。

对于并发模型《7周7并发模型》这本书讲的非常好。推荐阅读。书中展示了七种最经典的并发模型和大量的编码实例。

 

为啥要并发

把事情设计为【并发】有什么好处呢?假如能同时干活的人只有1个,其实并没有什么好处。【并发】的方法的总耗时总会>=串行的方法。因为【并发】或多或少总会引入需要协作和沟通成本。最小的代价就是不需要沟通,此时【并发】的方法和串行的方法工作量是一样的。

但是【并发】的巨大优势是在可以干活的人数量变多时,马上得到【并行】的好处。假如我们可以得到一个【并发】的【P进程】,并且真的为其配备足够多的人,那么做事的效率就会高很多。回到软件系统,假如有一个【并发】的【程序】,它在只有1个CPU的核心的机器上可以跑,在2个的CPU的也可以跑,在4核CPU上也可以跑。物理上可用CPU核心越多,程序能够越快执行完。而不管在哪里跑,程序本身不用做变化。编程是一件成本很高的事,能够做到程序不变而适应各种环境,可以极大的降低开发成本。你能想象下为1核心CPU开发的Office软件和4核心的不一样吗?

 

并发和并行的关系是什么

【并发】(Concurrency)这个词的本意是指两件事没有谁先谁后的关系,或者说关系不确定。举个通俗的例子,自然数任何两个数字都可以比较大小。我们可以明确地说5 > 3。但是如果换一个领域,并不是任何两个元素都有明确的顺序关系,或者说“谁在前面谁在后面都是可以的“。

对于任务执行这个领域,对于两个任务A和B,如果我们说他们俩是【并发】的,这就要求不能在任务B里使用A的结果,也不能让A执行时使用B的结果。因此在执行层面,A可以在B之前执行,也可以在B执行,或者A和B交替执行,或者A和B【并行】的执行。不管执行层面怎么折腾,结果都是对的。

反过来,如果A的执行需要B的结果,那也就意味着A和B不是【并发】的,必须让B先执行完,A才可以开始。在实现层面,就可以用加锁、channel等方式来表达“先B后A”。

Rob Pike在一个Talk里(blog.golang.org/concurr)提到了很重要的两个观点:

  • Concurrency is not Parallelism
  • Concurrency enables parallelism & makes parallelism (and scaling and everything else) easy

前一个观点【并发】和【并行】不是一件事,我们都可以理解了。【并发】说的是处理(Deal)的方法;【并行】说的是执行(Execution)的方法。

后一个观点指的是,如果想让一个事情变得容易【并行】,先得让制定一个【并发】的方法。倘若一个事情压根就没有【并发】的方法,那么无论有多少个可以干活的人,也不能【并行】。比如你让20个人不铺路,而是一起去拧同一个灯泡,也只能有一个人踩在梯子上去拧,其他19个人只能看着,啥也干不了。

对于一个问题,能不能找到【并发】的办法,取决于问题本身。有些问题很容易【并发】,有些问题可以一部分【并发】其余的串行(比如对数组排序就是,无论怎么拆,最终也要把每个拆开的问题结果合并到一起再排序才行),有些问题则根本上就不能【并发】。找不到【并发】的方法也就意味着不管有多少CPU核心,也没法【并行】执行。

换一个极端,假如为最多20个人设计了【并发】的方法,结果来了40个人,就意味着40人里有20个人是闲着的,是浪费。也就是说【并行】的上限是由【并发】的方法的设计决定的。这就解释了你吃鸡的时候,4核CPU和8核差别不大,因为这个游戏压根就没设计成可以利用这么多个CPU核心。(BTW,但游戏被设计为能充分利用显卡的多核心)

其实上面只是将CPU核心当作是“做事的人“,再广义一点,比如显卡,网卡,磁盘都是独立的可以干活的人。这些组件之间也可以并行的跑。因此,在设计程序的时候,可以比如把计算和IO任务拆开设计一个【并发】的方法,然后利用CPU和网卡是两个零件来【并行】的跑

常见的误解

你可能看到过下面的论断:

并发是多个任务交替使用CPU,同一时刻只有一个任务在跑;并行是多个任务同时跑

这个理解不能说全错,但是合到一起就形成了错误的理解。这个错误的理解就是:并发和并行是两个并列的,非此即彼的概念,一个状态要不就是并行的,要不就是并发的。这是完全错误的,实际上看到上面的解释你就会发现【并发】和【并行】描述的是两个频道的事情。正如Rob Pike所言,一个是“如何处理”,一个是“如何执行”。因此,对于:

并发是多个任务交替使用CPU,同一时刻只有一个任务在跑

其实正确的理解是:针对一个问题,想到了一个可以拆解为多个【并发】的任务,这些任务执行时因为只有一个CPU只能“切换”的跑。

对于:

并行是多个任务同时跑

其实的意思是:如果这些并行执行的任务是解决同一个问题的,那么他们既是【并发】的,同时也是【并行】的。

那么可不可以做到只【并行】,而不【并发】呢?当然可以,但这也就意味着【并行】的程序之间没有什么关联,各干各的,就像大街上来来往往的陌生人一样。这的确是【并行】,并且是这个世界的常态。但是一群不认识的,各干各的人是不能一起解决问题的,要一起就得有同一个目标,制定一套沟通的方法,形成【并发】的方案。这种形式在现实当中就是“公司”。

引言

今年七月的实习太划水了,每天下班后也没有充电,全去打守望先锋了,仔细想想这才是最关键的一个月,应该奋斗而不是享乐,挑战周五看完这本Guide哥推荐的 Java并发实现原理!!!

这本书初看,并不喜欢,但当看完第一章后,我改变了想法。任何一本书都不可能知识体系全面,我们也正因此去不断学习。

阅读学习,更应该看重其知识广度,因为对于现阶段的我来说,还没有追求深度的能力,所以,加油吧!!!

 

多线程基础(个人笔记)

1.1 线程的优雅关闭

1.1.1 stop()和destory()函数

何谓线程?即 一段运行中的代码

若 QQ 是一个进程,我们与好友的聊天框作为 QQ 进程下的一个线程,有一天,我们可能和女朋友聊着聊着就吵起来了,这个时候心里一肚子火气,我们不想通过正常的点击 关闭 按钮关闭对话框,而想强制杀死线程,那么问题来了

我们能否将运行到一般的线程强制杀死?

答:不能。在Java中,有stop()destory()函数是用于强制杀死线程的,但官方明确不建议使用,因为,如果强制杀死线程,则线程中所使用的资源,例如文件描述符、网络连接等不能正常关闭。

因此,一个线程一旦运行起来,就不要去强行打断它,合理的关闭办法是让其运行完(也就是函数执行完毕),干净地释放掉所有资源,然后退出。如果是一个不断循环运行的线程,就需要用到线程间的通信机制,让主线程通知其退出。


1.1.2 守护线程

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务等待处理某些发生的事件

默认开启的线程都是非守护线程,可通过 t1.setDaemon(true) 将 非守护线程转为 守护线程Java 中有一个规定:当所有的非守护线程退出后,整个 JVM 进程就会退出。意思就是 守护线程 不算作“数”,守护线程不影响整个 JVM 进程的退出,例如垃圾回收线程。


1.1.3 设置关闭的标志位

通过设置标志位跳出循环,避免阻塞(死循环)。

通过标志位来实现,有个问题,即如果程序没有机会再执行while(!stopped) 代码即无法再执行到标志位判断,则一直无法退出循环。

此时就需要使用 InterruptedException()与interrupt()函数。


1.2 InterruptedException()和interrupt ()函数

1.2.1 什么情况下会抛出 Interrupted 异常

首先注意:Interrupt 不是说一个线程运行到一半,把它中断了,然后抛出 InterruptExcetion 异常

如果没有声明会抛出 InterruptedExcetion ,则 t.interrupt()  不会抛出异常,

只有那些声明了会抛出 InterruptedException 的函数才会抛出异常也就是下面这些常用的函数:

先声明抛出 InterruptedException,Interrupt() 函数才有效!!!


1.2.2 轻量级阻塞和重量级阻塞

不仅锁分为轻量级锁、重量级锁,阻塞也分为 轻量级阻塞重量级阻塞

轻量级阻塞:能被中断的阻塞称为 轻量级阻塞,对应的线程状态为 WAITING 或者 TIMED_WAITING;

重量级阻塞:像 Synchronized 这种不能被中断的阻塞称之为 重量级阻塞,对应的状态是BLOCKED。

 

线程的状态迁移过程

无阻塞函数:READY <–>RUNNING

阻塞函数:WAITING <–>TIMED_WAITING

如果使用了 Synchronized 关键字或块 :进入 BLOCKED 

除了常用的阻塞/唤醒函数,还有一对不太常见的阻塞/唤醒函数,LockSupport.park()/unpark()。这对函数非常关键,Concurrent包中Lock的实现即依赖这一对操作原语。故而t.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。


1.2.3 t.isInterrupted()与Thread.interrupted()的区别

这两个函数都是线程用来判断自己是否收到过中断信号前者是非静态函数,后者是静态函数。二者的区别在于前者只是读取中断状态不修改状态后者不仅读取中断状态,还会重置中断标志位


1.3 synchronized关键字

1.3.1 锁的对象是什么(即锁住的到底是什么)

误解:它通常加在所有的静态成员函数和非静态成员函数的前面,表面看好像是“函数之间的互斥”

实质:给某个对象加了把锁

等价于对于非静态成员函数其实是加在对象 a 上面的;对于静态成员函数加在 A.class面的;当然,class 本身也是对象。

补充:一个静态成员函数和一个非静态成员函数,都加了 synchronized 关键字,分别被两个线程调用,它们是否互斥?很显然,因为是两把不同的锁,所以不会互斥


1.3.2 锁的本质是什么

多个线程要访问同一个资源。线程是一段段运行的代码;资源就是一个变量,一个对象,或者一个文件等;而就是要实现 线程 资源的访问控制,保证同一时间只能有一个线程去访问某个资源。

实现对资源的访问控制不仅有锁,还有信号量:如果同一时间允许多个线程访问同一资源,那么锁就变成信号量从程序角度看,本身就是一个对象,它维护三个信息

1.自己(该锁)是否被某个线程占用(0没有,1被占用)

2.占用自己(该锁)的线程ID

3.其他阻塞的、等待拿自己(该锁)的线程列表

锁是一个对象,要访问的共享资源也是一个对象,合二为一,成为一个对象,使得在 Java 里面,synchronized 关键字可以加在任何对象的成员上面,这意味着,这个对象既是共享资源同时也具备 “锁”的功能


1.3.3 synchronized 实现原理

答案在Java的对象头里在对象头里,有一块数据叫Mark Word在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位占用该锁的thread ID


1.4 wait()与notify()

1.4.1 生产者、消费者模型满足该模型的条件:

(1)内存队列本身要加锁,才能实现线程安全。

(2)阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。

(3)双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者。

1.   如何阻塞、如何通知?

办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。

办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。0

2.如何双向通知?

办法1:wait()与notify()机制。

办法2:Condition机制。


1.4.2 为什么wait() 和 notify()必须和 synchronized 一起使用?

注:wait() 和 notify()是 Object 的成员函数,是基础中的基础,而非作为像 Thread 一类的成员函数

两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),该对象本身就需要同步!

所以,在调用wait()、notify()之前,要先通过 synchronized 关键字同步给对象,也就是给该对象加锁。前面已经讲了,synchronized 关键字可以加在任何对象的成员函数上面任何对象都可能成为锁。那么,wait()和notify()要同样如此普及,也只能放在 Object 里面


1.4.3 为什么 wait()的时候必须释放锁?

为了避免死锁!

线程 A 进入 synchronized(obj 1)中之后,也就是对 obj 1 上了锁此时,调用 wait()进入阻塞状态,则一直不能退出 synchronized 代码块;那么线程 B 永远也无法进入 synchronized(obj 1)同步块里永远没有机会调用 notify(),则造成死锁


1.4.4 wait()与notify()的问题 

生产者本来只想通知消费者,但它把其他的生产者也通知了;消费者本来只想通知生产者,但它被其他的消费者通知了。原因就是 wait() notify()所作用的对象和 synchronized 所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件这正是Condition要解决的问题。


1.5  volatile关键字

1.5.1 64位写入的原子性(Half Write)

对于一个long型变量的赋值和取值操作而言,在多线程场景下,线程 A 调用set(100),线程B 调用 get(),在某些场景下,返回值可能不是100.

因为 JVM 的规范并没有要求 64位的 long 或者 double 的写入是原子的;在32位的机器上,一个64位变量的写入可能被拆分成两个32位的写操作来执行,如此一来,读取的线程可能读到“一半的值”,解决办法为:在 long 前面加上 volatile 关键字

即,long 或 double的写入操作不一定原子性,多线程下可能产生读到写入一半的值解决办法加上 volatile 关键字(可见性、禁止重排序)


1.5.2 内存可见性

多线程下,我们可能会遇到“最终一致性”和“强一致性”分别对应的情况,举个例子:

线程 A 负责标志位的修改,Flag 标志位类型为 Boolean 类型

线程 B 负责读取 Flag 标志位的值

线程 A 将 Flag 设置为 true 的时候,线程 B 读到的可能还是 false,可是过一段时间之后,线程 B 再读到的却是 true 了这就是满足“最终一致性”,而不满足“强一致性”

强一致性当线程 A 将 Flag 设置为 true 的时候,线程 B 读到的必须也立即是 true

0 73

线程池的submit和execute方法区别(转载)

线程池中的execute方法大家都不陌生,即开启线程执行池中的任务。还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果。下面简要介绍一下两者的三个区别:

1、接收的参数不一样

2、submit有返回值,而execute没有

用到返回值的例子,比如说我有很多个做validation的task,我希望所有的task执行完,然后每个task告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。
然后我就可以把所有失败的原因综合起来发给调用者。

个人觉得cancel execution这个用处不大,很少有需要去取消执行的。

而最大的用处应该是第二点。
3、submit方便Exception处理
意思就是如果你在你的task里会抛出checked或者unchecked exception,
而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。

下面一个小程序演示一下submit方法

public class RunnableTestMain {

public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);

/**
* execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
*/
pool.execute(new RunnableTest(“Task1”));

/**
* submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。请看下面:
*/
Future future = pool.submit(new RunnableTest(“Task2”));

try {
if(future.get()==null){//如果Future’s get返回null,任务完成
System.out.println(“任务完成”);
}
} catch (InterruptedException e) {
} catch (ExecutionException e) {
//否则我们可以看看任务失败的原因是什么
System.out.println(e.getCause().getMessage());
}

}

}

public class RunnableTest implements Runnable {

private String taskName;

public RunnableTest(final String taskName) {
this.taskName = taskName;
}

@Override
public void run() {
System.out.println(“Inside “+taskName);
throw new RuntimeException(“RuntimeException from inside ” + taskName);
}

}

0 109


/**
 * @Author Chengzhi
 * @Date 2021/4/19 9:23
 * @Version 1.0
 *
 * 观察测试线程的状态
 */
public class TestState {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("你好啊!~~");
                }
            }
        });

        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);//NEW

        //观察启动后
        thread.start();
        System.out.println("当前线程状态为:" + thread.getState());//Run

        while(state != Thread.State.TERMINATED){//只要线程不终止,就一直输出状态
            Thread.sleep(100);
            state = thread.getState();//更新线程状态
            System.out.println("当前线程状态为:" + state);//输出线程状态
        }

    }
}

注意:线程死亡之后不能再调用start方法!!!(因为start是就绪,等待cpu调度即可执行而线程死亡代表线程已经结束)

0 98

线程休眠:

  • sleep(Time)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪 状态
  • sleep可以模拟网络延时,倒计时等,模拟网络延时可以帮助发现问题
  • 每一个对象都有一个锁,sleep不会释放锁

2. sleep() 与 interrupt()

public static native void sleep(long millis) throws InterruptedException;
public void interrupt();

sleep(long millis): 睡眠指定时间,程序暂停运行,睡眠期间会让出CPU的执行权,去执行其它线程,同时CPU也会监视睡眠的时间,一旦睡眠时间到就会立刻执行(因为睡眠过程中仍然保留着锁,有锁只要睡眠时间到就能立刻执行)。

sleep(): 睡眠指定时间,即让程序暂停指定时间运行,时间到了会继续执行代码,如果时间未到就要醒需要使用interrupt()来随时唤醒
interrupt(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去
public static void main(String[] args) throws Exception {
Thread thread0 = new Thread(()-> {
try {
System.out.println(new Date() + “\t” + Thread.currentThread().getName() + “\t太困了,让我睡10秒,中间有事叫我,zZZ。。。”);
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println(new Date() + “\t” + Thread.currentThread().getName() + “\t被叫醒了,又要继续干活了”);
}
});
thread0.start();

// 这里睡眠只是为了保证先让上面的那个线程先执行
Thread.sleep(2000);

new Thread(()-> {
System.out.println(new Date() + “\t” + Thread.currentThread().getName() + “\t醒醒,醒醒,别睡了,起来干活了!!!”);
// 无需获取锁就可以调用interrupt
thread0.interrupt();
}).start();
}

4. sleep() 与 wait()

① Thread.sleep(long millis): 睡眠时不会释放锁
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();

new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + “\t” + Thread.currentThread().getName() + “\t” + i);
try { Thread.sleep(1000); } catch (InterruptedException e) { }
}
}
}).start();

Thread.sleep(1000);

new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(new Date() + “\t” + Thread.currentThread().getName() + “\t” + i);
}
}
}).start();
}

object.wait(long timeout): 会释放锁

public class SleepWaitTest { public static void main(String[] args) throws InterruptedException { SleepWaitTest object = new SleepWaitTest();

    new Thread(() -> {
        synchronized (object) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
            try {
                object.wait(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
        }
    }).start();

	 // 先上面的线程先执行
    Thread.sleep(1000);

    new Thread(() -> {
        synchronized (object) {
            for (int i = 0; i < 5; i++) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
            }
        }
    }).start();
}
}

因main方法中Thread.sleep(1000)所以上面的线程Thread-0先被执行,当循环第一次时就会Thread.sleep(1000)睡眠,因为sleep并不会释放锁,所以Thread-1得不到执行的机会,所以直到Thread-0执行完毕释放锁对象lock,Thread-1才能拿到锁,然后执行Thread-1;

原文链接:https://blog.csdn.net/vbirdbest/article/details/81282163

0 92

最近学习Java多线程的时候了解到线程停止这一块的内容,为加深印象,搜索资料并整理如下

线程停止的三种方式

(1)使用退出标志(flag)使线程正常终止,也就是当run方法完成后线程终止。

  (2)使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend、resume一样,都是过期作废的方法。

(3)使用interrupt方法中断线程。

 

 

首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,
具体到底中断还是继续运行,应该由被通知的线程自己处理。

具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
也就是说,一个线程如果有被中断的需求,那么就可以这样做。
① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)

Thread thread = new Thread(() -> {
    while (!Thread.interrupted()) {
        // do more work.
    }
});
thread.start();

// 一段时间以后
thread.interrupt();

具体到你的问题,Thread.interrupted()清除标志位是为了下次继续检测标志位。
如果一个线程被设置中断标志后,选择结束线程那么自然不存在下次的问题,
而如果一个线程被设置中断标识后,进行了一些处理后选择继续进行任务,
而且这个任务也是需要被中断的,那么当然需要清除标志位了。

转自:https://www.zhihu.com/question/41048032/answer/89431513

0 123

这篇关于Volatile的文章解开了我关于多线程下可能出现的程序问题如何解决的困惑,转载http://www.cnblogs.com/dolphin0520/p/3920373.html,参考资料https://blog.csdn.net/fu123123fu/article/details/79794017

个人总结其关键点:

1.为了提高指令运行速度,CPU出现了告诉缓存,而高速缓存的出现导致了多线程情况下的缓存不一致问题

2.共享变量:被多个线程访问的变量称为共享变量

3.解决缓存不一致问题的办法:总线上加LOCK锁,或者通过缓存一致性协议(MESI)

4.加锁能解决,但是效率低,于是出现缓存一致性协议,MESI

5.MESI核心思想:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中 也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行设置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

6.并发编程三个概念:原子性问题,可见性问题,有序性问题

原子性:即一个操作或多个操作,要么全部执行且不被打断,要么都不执行,转账

可见性:当多个线程访问同一个变量(共享变量)时,一个线程修改了这个变量的值,那么其他的线程应该立即就能看到修改的值

有序性:即程序执行的顺序按照代码的先后顺序执行(可能会发送指令重排序)

7.指令重排序:发生在JVM层面,一般来说处理器为了提高程序运行效率可能会对输入代码进行优化,它不保证程序中的各个语句的执行先后顺序同代码中的顺序一致,但是它保证程序最终执行结果和代码顺序执行的结果是一样的,即不影响结果的优化,它不会影响单个线程的执行,但是会影响到线程并发执行的正确性

也就是说,如果想要并发程序正确地执行,必须保证原子性,可见性,有序性。只要有一个没有被保证,那么就可能导致程序运行不正确

8.Java内存模型为我们提供的方法和机制保证多线程编程的正确性

原子性:在Java中,对基本数据类型的读取和赋值操作是原子操作,这些操作不可被中断,要么被执行,要么不执行

只有x = 10这样的才是原子性操作,直接将数值10付给X,把数值10直接写入到工作内存

可见性:volatile关键字来保证可见性。若共享变量被volatile修饰,它会保证修改的值会立即被更新到主存,当其他线程需要读取时,它会取内存中读取新值;同时Synchronized和Lock也能保证可见性,这二者是保证同一时刻只有一个线程获得锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到内存当中

有序性:volatile可以保证一定的有序性,同样的synchronized和Lock也能保证有序性,syrnchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于让线程顺序执行同步代码,自然就保证了有序性

9.happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

===================Volatile来了!!!================

10.如果一个共享变量(类的成员变量,类的静态成员变量)使用了Volatile关键字,那么

保证了不同线程对这个变量进行操作时的可见性,也就是一个线程修改了某个变量的值,这个新的值对其他线程来说是立即可见的(缓存行失效)

禁止进行指令重排序(保证在volatile之前的语句一定先执行,volatile之后的一定后执行)

尤其注意:volatile无法保证原子性!!!但可以保证有序性

11.其原理和实现机制

12.使用Volatile关键字的场景

 

 

volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。

以下是本文的目录大纲:

一.内存模型的相关概念

二.并发编程中的三个概念

三.Java内存模型

四..深入剖析volatile关键字

五.使用volatile关键字的场景

一.内存模型的相关概念

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

1 i = i + 1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

1)通过在总线加LOCK#锁的方式

2)通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

二.并发编程中的三个概念

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

1.原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

同样地反映到并发编程中会出现什么结果呢?

举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

1 i = 9;

假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

举个简单的例子,看下面这段代码:

  1. //线程1执行的代码
  2. int i = 0;
  3. i = 10;
  4. //线程2执行的代码
  5. j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

  1. int i = 0;
  2. boolean flag = false;i = 1;                //语句1  
  3. flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

  1. int a = 10;    //语句1
  2. int r = 2;    //语句2
  3. a = a + 3;    //语句3
  4. r = a*a;     //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

 

那么可不可能是这个执行顺序呢: 语句2   语句1    语句4   语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

  1. //线程1:
  2. context = loadContext();   //语句1
  3. inited = true;             //语句2
  4. //线程2:
  5. while(!inited ){
  6.   sleep()
  7. }
  8. doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

三.Java内存模型

在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

举个简单的例子:在java中,执行下面这个语句:

i  = 10;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

1.原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

请分析以下哪些操作是原子性操作:

  1. x10;         //语句1
  2. y = x;         //语句2
  3. x++;           //语句3
  4. x = x + 1;     //语句4

咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

所以上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2.可见性

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

这8条原则摘自《深入理解Java虚拟机》。

这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

下面我们来解释一下前4条规则:

对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

四.深入剖析volatile关键字

在前面讲述了很多东西,其实都是为讲述volatile关键字作铺垫,那么接下来我们就进入主题。

1.volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

先看一段代码,假如线程1先执行,线程2后执行:

  1. //线程1
  2. boolean stop = false;
  3. while(!stop){
  4.     doSomething();
  5. }
  6. //线程2
  7. stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰之后就变得不一样了:

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

2.volatile保证原子性吗?

从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

下面看一个例子:

  1. public class Test {
  2.     public volatile int inc = 0;
  3.     public void increase() {
  4.         inc++;
  5.     }
  6.     public static void main(String[] args) {
  7.         final Test test = new Test();
  8.         for(int i=0;i<10;i++){
  9.             new Thread(){
  10.                 public void run() {
  11.                     for(int j=0;j<1000;j++)
  12.                         test.increase();
  13.                 };
  14.             }.start();
  15.         }
  16.         while(Thread.activeCount()>1)  //保证前面的线程都执行完
  17.             Thread.yield();
  18.         System.out.println(test.inc);
  19.     }
  20. }

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

3.volatile能保证有序性吗?

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

  1. //x、y为非volatile变量
  2. //flag为volatile变量
  3. x = 2;        //语句1
  4. y = 0;        //语句2
  5. flag = true;  //语句3
  6. x = 4;         //语句4
  7. y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

那么我们回到前面举的一个例子:

  1. //线程1:
  2. context = loadContext();   //语句1
  3. inited = true;             //语句2
  4. //线程2:
  5. while(!inited ){
  6.   sleep()
  7. }
  8. doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

4.volatile的原理和实现机制

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

五.使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

0 60

查阅资料时常常看到线程安全和线程不安全的字样,特此记录二者的详细定义,同时讨论线程安全我们有必要了解什么是原子操作,什么是原子类以及同步,非同步和阻塞非阻塞的区别

 

 

1.同步与异步

编辑

同步和异步 [1]  关注的是消息通信机制 (synchronous communication/ asynchronous communication)。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

2.阻塞与非阻塞

编辑

阻塞和非阻塞 [2]  关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

3.结合两概念分析

编辑

  • 进程通信 [3]  层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。
  • 发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步) 是互不影响的。
  • 在 IO 系统调用层面( IO system call )层面, 非阻塞 IO 系统调用  异步 IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )
  • 非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。

 

原子操作:

原子操作(atomic operation)是不需要synchronized”,这是多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切 [1]  换到另一个线程)

定义

如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
将整个操作视作一个整体是原子性的核心特征。

特性

原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是” 原子操作”,因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl (递减指令)为例,这是一个典型的”读-改-写”过程,涉及两次内存访问。设想在不同CPU运行的两个进程都在递减某个计数值,可能发生的情况是:
⒈ CPU A(CPU A上所运行的进程,以下同)从内存单元把当前计数值⑵装载进它的寄存器中;
⒉ CPU B从内存单元把当前计数值⑵装载进它的寄存器中。
⒊ CPU A在它的寄存器中将计数值递减为1;
⒋ CPU B在它的寄存器中将计数值递减为1;
⒌ CPU A把修改后的计数值⑴写回内存单元。
⒍ CPU B把修改后的计数值⑴写回内存单元。
我们看到,内存里的计数值应该是0,然而它却是1。如果该计数值是一个共享资源的引用计数,每个进程都在递减后把该值与0进行比较,从而确定是否需要释放该共享资源。这时,两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它–两个进程都推断出:计数值是1,共享资源仍然在被使用。

线程安全:

 

一、线程安全

是指你的代码所在的进程中有多个线程同时运行,而这些线程可能会同时运行这段代码,如果每次运行的代码结

果和单线程运行的结果是一样的,而且其他变量的值和预期的也是一样的,那么就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,

也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行

写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

二、非线程安全

是不提供代码数据访问保护,可能出现多个线程先后访问更改数据造成所得的数据是脏数据。*(脏数据是表示一个数据已经

被修改,但是还没有保存或进一步的处理。例如你操作数据库修改某一字段内容,在你修改了但还没commit时,另一线程在读取这

数据,他读取的就是你修改前的数据,但事实上你已经修改了,这就是脏数据了。)*在多个线程同时访问同一个对象时会发生数据

错误 不完整等情况时 那就是线程不安全,不会发生上续错误时是线程安全的,一般采用锁机制。

三、线程安全出现的根本原因

存在两个或者两个以上的线程对象共享同一个资源;

多线程操作共享资源代码有多个语句。

存在竞争的线程不安全,不存在竞争的线程就是安全的

 

 

存在线程安全问题必须满足三个条件:

1.有共享变量

2.处在多线程环境下

3.共享变量有修改操作。

所谓解决线程安全问题无非就是将操作原子化,正如楼上各位所说的加sychronized,或者加lock什么的,只要将操作原子化就能避免线程安全的问题。但加锁会有性能问题。

所以在多线程情况下,优先考虑能否不用共享,优先使用局部变量代替共享的全局变量。

只能用共享变量的时候优先使用原子类,诸如AtomicInteger尔尔。

没有原子类,可以自己创造自己的原子类。

如以上方法都不能奏效,再考虑使用sychro,lock之类尔尔。

别一上来就sychro,不仅low,而且显的不够专业

 

四、怎么实现线程安全

1、synchronized
同步代码块、同步方法

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完

整性,使用方法一般是加在方法上。

public class ThreadDemo {

int count = 0; // 记录方法的命中次数

public synchronized void threadMethod(int j) {

count++ ;

int i = 1;

j = j + i;
}
}

这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

2.Lock
Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块

五、分别在什么情况下使用

线程安全是多线程操作同一个对象不会有问题;

非线程安全是指多线程操作同一个对象可能会出现问题;

线程安全会导致性能的降低。

如果是多个线程操作同一个对象,那么使用线程安全的Vector;否则,就使用效率更高的ArrayList。

PS(非线程安全并非不安全)

非线程安全并不是多线程环境下就不能使用。

 

六、集合线程安全实例

1.Vector
Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它给几乎所有的public方法都加上了synchronized关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用

2.HashTable
HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了

3.Collections包装方法
Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());
Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

4.java.util.concurrent包中的集合
4.1ConcurrentHashMap
ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率

4.2CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

5.除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到

 

转载整理自:

————————————————
版权声明:本文为CSDN博主「写代码的彭于晏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lixiaobuaa/article/details/79689338

————————————————
版权声明:本文为CSDN博主「CodeHunter_qcy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43390235/article/details/90897699

作者:花溪的小石头
链接:https://www.zhihu.com/question/49855966/answer/298161490
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 132

Thread使用start和run方法启动线程有什么区别?

run()

start()

run(): 调用线程的run方法,就是普通的方法调用,虽然将代码封装到两个线程体中,可以看到线程中打印的线程名字都是main主线程,run()方法用于封装线程的代码,具体要启动一个线程来运行线程体中的代码(run()方法)还是通过start()方法来实现,调用run()方法就是一种顺序编程不是并发编程。

start():启动一个线程,线程之间是没有顺序的,是按CPU分配的时间片来回切换的。 run():执行结果都是main主线程;

相信Thread对我们来说并不陌生,学java的时候都是入门的东西,我们一般使用thread都是new一个线程,然后调用start方法启动,使用start方法才真正实现了多线程运行,因为这个时候不用等待我们的run方法执行完成就可以继续执行下面的代码,这才叫多线程嘛!因为thread线程有5种状态,创建-就绪-运行-阻塞-死亡这五种,那么我们的start方法呢就是就绪这一步,因为这个时候我们的线程并没有立即的执行,而是得等待,等到我们的cpu有空闲的时候,才会执行线程里面的run方法,等run方法执行完了,线程就结束了。

那么我们直接使用thread执行run方法会咋样呢?因为run方法是thread里面的一个普通的方法,所以我们直接调用run方法,这个时候它是会运行在我们的主线程中的,因为这个时候我们的程序中只有主线程一个线程,所以如果有两个线程,都是直接调用的run方法,那么他们的执行顺序一定是顺序执行,所以这样并没有做到多线程的这种目的。

 

0 133

什么是线程(什么是串行和并行)?

串行,其实是相对于单条线程来执行多个任务来说的;拿下载文件举例子:当我们下载多个文件时,在串行中它是按照一定的顺序去下载的,也就说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。

并行:下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间是重叠的。

在这里插入图片描述

什么是多线程?

举例:打开腾讯管家、腾讯管理本身就是一个程序,也就是说它是一个进程,它里面有很多的功能,如查杀病毒、清理垃圾、电脑加速等众多功能。

按照单线程来说,无论你想要清理垃圾、还是要查杀病毒、你都必须先做完其中的一件事,才能做下一件事,这里面有一个执行顺序;

如果是多线程的话,我们其实在清理垃圾的时候,还可以进行病毒查杀、电脑加速等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。在这里插入图片描述

以上就是,一个进程运行时产生了多个线程。

了解了什么是多线程之后,我们需要了解使用多线程必须考虑的问题—线程安全。

什么是线程安全?

当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。

Integer count = 0;
public void getCount() {
       count ++;
       System.out.println(count);
 }

很简单的一段代码,下面我们就来统计一下这个方法的访问次数,多个线程同时访问会不会出现什么问题,我开启的3条线程,每个线程循环10次,得到以下结果:
在这里插入图片描述

我们可以看到,这里出现了两个26,出现这种情况显然表明这个方法根本就不是线程安全的,出现这种问题的原因有很多。

最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来,还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

那么由此我们可以了解到,这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量。其实要对线程安全问题给出一个明确的定义,还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。

当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

搞清楚了什么是线程安全,接下来我们看看Java中确保线程安全最常用的两种方式。先来看段代码。

public void threadMethod(int j) {

    int i = 1;

    j = j + i;
}

大家觉得这段代码是线程安全的吗?

毫无疑问,它绝对是线程安全的,我们来分析一下,为什么它是线程安全的?

我们可以看到这段代码是没有任何状态的,就是说我们这段代码,不包含任何的作用域,也没有去引用其他类中的域进行引用,它所执行的作用范围与执行结果只存在它这条线程的局部变量中,并且只能由正在执行的线程进行访问。当前线程的访问,不会对另一个访问同一个方法的线程造成任何的影响。

两个线程同时访问这个方法,因为没有共享的数据,所以他们之间的行为,并不会影响其他线程的操作和结果,所以说无状态的对象,也是线程安全的。

添加一个状态呢?

如果我们给这段代码添加一个状态,添加一个count,来记录这个方法并命中的次数,每请求一次count+1,那么这个时候这个线程还是安全的吗?

public class ThreadDemo {

   int count = 0; // 记录方法的命中次数

   public void threadMethod(int j) {

       count++ ;

       int i = 1;

       j = j + i;
   }
}

明显已经不是了,单线程运行起来确实是没有任何问题的,但是当出现多条线程并发访问这个方法的时候,问题就出现了,我们先来分析下count+1这个操作。

进入这个方法之后首先要读取count的值,然后修改count的值,最后才把这把值赋值给count,总共包含了三步过程:“读取”一>“修改”一>“赋值”,既然这个过程是分步的,那么我们先来看下面这张图,看看你能不能看出问题:
在这里插入图片描述
可以发现,count的值并不是正确的结果,当线程A读取到count的值,但是还没有进行修改的时候,线程B已经进来了,然后线程B读取到的还是count为1的值,正因为如此所以我们的count值已经出现了偏差,那么这样的程序放在我们的代码中,是存在很多的隐患的。

如何确保线程安全?

既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?我们说说常见的几种方式

synchronized

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

public class ThreadDemo {

   int count = 0; // 记录方法的命中次数

   public synchronized void threadMethod(int j) {

       count++ ;

       int i = 1;

       j = j + i;
}
}

这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

lock

先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类

   private void method(Thread thread){
       lock.lock(); // 获取锁对象
       try {
           System.out.println("线程名:"+thread.getName() + "获得了锁");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("线程名:"+thread.getName() + "释放了锁");
           lock.unlock(); // 释放锁对象
       }
   }

进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

写个主方法,开启两个线程测试一下我们的程序是否正常:

public static void main(String[] args) {
       LockTest lockTest = new LockTest();

       // 线程1
       Thread t1 = new Thread(new Runnable() {

           @Override
           public void run() {
               // Thread.currentThread()  返回当前线程的引用
               lockTest.method(Thread.currentThread());
           }
       }, "t1");

       // 线程2
       Thread t2 = new Thread(new Runnable() {

           @Override
           public void run() {
               lockTest.method(Thread.currentThread());
           }
       }, "t2");

       t1.start();
       t2.start();
   }

结果
在这里插入图片描述
可以看出我们的执行,是没有任何问题的。

其实在Lock还有几种获取锁的方式,我们这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

我们来看下代码:

private void method(Thread thread){
       // lock.lock(); // 获取锁对象
       if (lock.tryLock()) {
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");
               // Thread.sleep(2000);
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
   }

结果:我们继续使用刚才的两个线程进行测试可以发现,在线程t1获取到锁之后,线程t2立马进来,然后发现锁已经被占用,那么这个时候它也不在继续等待。

在这里插入图片描述
似乎这种方法,感觉不是很完美,如果我第一个线程,拿到锁的时间,比第二个线程进来的时间还要长,是不是也拿不到锁对象?

那我能不能,用一中方式来控制一下,让后面等待的线程,可以等待5秒,如果5秒之后,还获取不到锁,那么就停止等,其实tryLock()是可以进行设置等待的相应时间的。

private void method(Thread thread) throws InterruptedException {
       // lock.lock(); // 获取锁对象

       // 如果2秒内获取不到锁对象,那就不再等待
       if (lock.tryLock(2,TimeUnit.SECONDS)) {
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");

               // 这里睡眠3秒
               Thread.sleep(3000);
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
   }

结果:看上面的代码,我们可以发现,虽然我们获取锁对象的时候,可以等待2秒,但是我们线程t1在获取锁对象之后,执行任务缺花费了3秒,那么这个时候线程t2是不在等待的。
在这里插入图片描述
我们再来改一下这个等待时间,改为5秒,再来看下结果:

private void method(Thread thread) throws InterruptedException {
       // lock.lock(); // 获取锁对象

       // 如果5秒内获取不到锁对象,那就不再等待
       if (lock.tryLock(5,TimeUnit.SECONDS)) {
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
   }

结果:这个时候我们可以看到,线程t2等到5秒获取到了锁对象,执行了任务代码。
在这里插入图片描述
以上就是使用Lock,来保证我们线程安全的方式。