Java并发编程:线程和锁的使用与解析
线程的使用
新建线程
新建一个线程有两种方法:继承Thread类,然后重写run方法;实现Runnable接口,然后实现run方法。实际上Thread类也是实现的Runnable接口,再加上类只能单继承,所以推荐使用Runnable接口。示例如下:
- class Demo1 implements Runnable{
- @Override
- public void run() {
- //新建线程需要执行的逻辑
- }
- }
- class Demo2 extends Thread{
- @Override
- public void run() {
- //新建线程需要执行的逻辑
- }
- }
对于Thread类,当然可以使用匿名内部类来简化写法:
- Thread thread=new Thread(){
- public void run(){
- //新建线程需要执行的逻辑
- }
- };
- //Lambda表达式简化后
- Thread thread=new Thread(()->{
- //需要执行的逻辑
- });
新建完一个线程后,就可以用对象实例来启动线程,启动后就会执行我们重写后的run方法:
- thread.start();
此外,Thread类有个非常重要的构造方法:
- public Thread(Runnable target) {
- init(null, target, "Thread-" + nextThreadNum(), 0);
- }
可见,传入的是一个Runnable类型的参数。为什么需要这个构造函数?因为Runnable接口只有一个run方法,如果我们直接实例化实现了这个接口的类,然后调用run方法,其实就和普通的类没有区别,并没有另外一个线程去执行run方法。说白了,Runnable并不是新建了一个线程,而只是线程里面执行任务的一种类型。在Java并发编程里,我们总是说的任务,很多时候就是Runnable类型的。所以我们还是需要把实现了Runnable接口的类的实例传入Thread的构造函数,然后通过start方法去调用Runnable的run方法。
- //新建一个任务(Demo1实现了Runnable接口)
- Demo1 task=new Demo1;
- //新建一个线程并传入需要执行的任务
- Thread thread=new Thread(task);
- //启动线程执行任务
- thread.start();
线程的其他方法
熟悉了线程的创建,再简单了解一下操作线程的其他方法。
stop方法:作用是终止线程,但不推荐使用,因为它是强制结束线程,不管线程执行到了哪一步,很容易造成错误数据,引起数据不一致的问题。
interrupt方法:作用和stop类似,但是并不会那么粗鲁的终止线程,如果只调用这一个方法并不会中断线程,它还需要配合一个方法使用:
- class Demo implements Runnable {
- @Override
- public void run() {
- //通过isInterrupted方法判断当前线程是否需要停止,不需要停止就执行逻辑代码
- while (!Thread.currentThread().isInterrupted()){
- //逻辑
- }
- }
- }
- public class Use {
- public static void main(String[] args) throws InterruptedException {
- Demo task = new Demo ();
- Thread thread=new Thread(task);
- thread.start();
- //通知thread可以终止了
- thread.interrupt();
- }
- }
wait方法和notify方法:这两个方法放在一起说,是因为它们需要配合使用。简单提一下synchronized ,这个会在在锁里面讲。synchronized大概的作用就是:代码块里的代码,同时只能由一个线程去执行,如何确保只有一个线程去执行?谁拥有锁谁就有资格执行。任何对象都可以调用wait方法,如obj.wait,它的意思就是让当前线程在obj上等待并释放当前线程占用的锁。obj.notify就是唤醒在obj上等待的线程并重新尝试获取锁。下面演示一下简单的使用:
- public class Use {
- //一定要确保等待和唤醒是同一个对象,用类锁也可以,至于什么是类锁可以看后面synchronized部分
- static Object object=new Object();
- static int i = 0;
- static class Demo1 implements Runnable {
- @Override
- public void run() {
- synchronized (object){
- for(int j=0;j<10000;j++){
- i++;
- if(i==5000){
- //1.因为t1先启动并进入同步代码块,所以首先输出5000
- System.out.println();
- try {
- //释放锁并等待
- object.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- //3.被唤醒后接着执行完剩余的代码,输出20000
- System.out.println(i);
- }
- }
- }
- static class Demo2 implements Runnable{
- @Override
- public void run() {
- synchronized (object){
- for(int j=0;j<10000;j++){
- i++;
- }
- //2.获取到t1释放的锁,执行完代码后输出15000并唤醒object上等待的线程
- System.out.println(i);
- object.notify();
- }
- }
- }
- public static void main(String[] args) throws InterruptedException {
- Demo1 task1 = new Demo1();
- Demo2 task2 = new Demo2();
- Thread thread1=new Thread(task1);
- Thread thread2=new Thread(task2);
- thread1.start();
- thread2.start();
- }
- }
需要注意的是,如果有多个线程在obj等待,只执行一次obj.notify的话,它是随机从obj等待列表中选择一个线程唤醒的,如果要唤醒所有等待线程,可以使用obj.notifyAll。不管wait、notify还是notifyAll只能在synchronized代码块中使用,否则会报IllegalMonitorStateException异常。Java之所以这么规定,是确保不会发生Lost Wake Up问题,也就是唤醒丢失。上面那个例子中使用了同步代码块,所以不会发生这种问题。试想一种情况,如果没有synchronized确保线程是有秩序执行的,当t2线程先唤醒了object上的对象,t1线程后暂停的,那么t1是不是就永远会暂停下去,t2的notify相当于丢失了,这就是Lost wake up。
join方法:作用是让指定线程加入当前线程。为了节约篇幅还是以interrupt方法的代码为例,如果在main方法里调用thread.join(),那么主线程就会等待thread线程执行完才接着执行。其实这就和单线程的效果差不多了。如果有时候thread线程执行时间太长,为了不影响其他线程,我们可以在join方法里传入一个时间,单位是毫秒,当过了这个时间不管thread线程有没有执行完,主线程都会接着执行。join方法其实是通过wait方法实现的,注意这个wait是被加入线程等待,而不是加入的线程等待。贴一下源码,逻辑很简单就不复述了,如果join不传入参数,millis默认就是0:
- public final synchronized void join(long millis)
- throws InterruptedException {
- long base = System.currentTimeMillis();
- long now = 0;
- if (millis < 0) {
- throw new IllegalArgumentException("timeout value is negative");
- }
- if (millis == 0) {
- while (isAlive()) {
- wait(0);
- }
- } else {
- while (isAlive()) {
- long delay = millis - now;
- if (delay <= 0) {
- break;
- }
- wait(delay);
- now = System.currentTimeMillis() - base;
- }
- }
- }
yield方法:这个方法会让出当前线程的CPU,让出后还会接着去争夺,但是还能不能争夺到就不一定了。一般优先级比较低的线程,为了节约资源可以适当调用这个方法。
线程安全
如何保证线程安全?无非就是锁的争夺。谁拥有了锁,谁才有资格执行。为什么只让一个线程执行代码?这就与Java的内存模型有关了。不了解的可以看其他资料,也可以看看我的另外一篇博客:https://www.cnblogs.com/lbhym/p/12458990.html,最后一节就是讲Java内存模型的。简单的说:线程共享的资源是放在一个共享内存区域的。当线程A去操作一个共享变量时,它会先把这个变量拷贝到自己私有的内存空间,然后进行操作,最后把操作后的值赋值到共享内存中的变量。如果在赋值之前另外一个线程B刚刚更新了这个值,那么线程A的操作就把线程B的操作给覆盖了,而线程B浑然不知,接着执行它的逻辑,这就造成了数据不一致的情况。所以我们必须加上一把锁,确保同一时间只能由一个线程来修改这个变量。
关键字synchronized
关键字synchronized的作用前面已经提到过了,下面给个简单的示例:
- class Demo implements Runnable {
- static int i = 0;
- @Override
- public void run() {
- //小括号中的Demo.class就是锁,大括号内的代码同时只能由一个线程执行
- synchronized (Demo.class){
- for(int j=0;j<10000;j++) {
- i++;
- }
- }
- }
- }
- public class Use {
- public static void main(String[] args) throws InterruptedException {
- Demo task = new Demo();
- Thread thread1=new Thread(task);
- Thread thread2=new Thread(task);
- thread1.start();
- thread2.start();
- //让两个线程加入主线程,这样就可以输出执行后的i了
- thread1.join();
- thread2.join();
- System.out.println(Demo.i);//输出20000,如果去掉同步代码块,i绝对小于20000
- }
- }
其实这个关键字的作用很好理解,关键在于,小括号里面有什么实际意义,它与Lock有什么区别?
首先synchronized和Lock都是Java里面的锁机制,前者用起来更加方便,后者功能更多。方便在哪?进入代码块前自动获取锁,如果锁已经被占,则会等待。执行完同步代码块中的内容,自动释放锁。而Lock需要手动加锁解锁,接下来会讲。
接着说说synchronized具体用法,小括号里就是锁对象。有一点需要注意,synchronized锁的是对象,而不是里面的代码,谁拥有指定的锁谁就能执行里面的代码。明白这一点有助于理解下面的内容。
synchronized的锁分为类锁和对象锁。它们的区别就是作用域的不同。
首先说说对象锁怎么用以及它的特点:
- //对象锁:
- synchronized(this){...}
- synchronized(类的实例){...}
- //修饰在void前也是对象锁
- public synchronized void run(){...}
如果synchronized里指定的是对象锁,那么在创建task时,不同的实例对象就是不同的锁。大家可以在上面示例代码的基础上,再用Demo类实例化一个task2,然后用thread去执行它,接着把synchronized小括号里的锁换成this,也就是对象锁,会发现输出的i小于20000。因为task和task2完全就是不同的锁,两个线程并不冲突,这就是为什么上面强调,锁的是对象,而不是里面的代码。
再说说类锁的用法和特点:
- //类锁
- synchronized(类名.class){...}
- //修饰在静态方法前也是类锁,run方法里直接调用handler就行
- private synchronized static void handler(){...}
上面的示例代码就是一个类锁,即使实例化两个不同的对象,提交给两个线程执行后,输出结果肯定是20000,也就是说它们是同步的。
最后说一点,同一个类中,类锁和对象锁依旧是不同的锁,它们之间互不干扰,不是同步的。举个例子:
- class Demo implements Runnable {
- static int i = 0;
- @Override
- public void run() {
- run2();
- run3();
- }
- //类锁
- private synchronized static void run2(){
- for(int j=0;j<10000;j++) {
- i++;
- }
- }
- //对象锁
- private synchronized void run3(){
- for(int j=0;j<10000;j++) {
- i++;
- }
- }
- }
main方法就不贴了,记得实例化一个task2给thread2执行。最后的输出结果肯定小于40000,如果把run3改成静态方法,也就是变成类锁,输出结果就是40000了。
接口Lock
Lock接口下提供了一套功能更完整的锁机制。如果项目中线程的竞争并不激烈,使用synchronized完全足够,如果竞争很激烈,还需要其他一些功能,这时候就可以尝试一下Lock提供的锁了。
ReentrantLock:可重入锁
简单的示例如下,说明也在注释当中:
- class ReenterLock implements Runnable {
- //可重入锁,意思是:在同一个线程中,可以对lock多次加锁,当然也必须解锁对应次数
- //那么Lock下的锁是类锁还是对象锁,取决于锁对象是类变量还是普通的全局变量,加上static就是类锁,反之就是对象锁
- static ReentrantLock lock = new ReentrantLock();
- static int i = 0;
- @Override
- public void run() {
- lock.lock();
- for (int j=0;j<10000;j++){
- i++;
- }
- lock.unlock();
- }
- }
- public class 可重入锁 {
- public static void main(String[] args) throws InterruptedException {
- ReenterLock task = new ReenterLock();
- Thread thread1=new Thread(task);
- Thread thread2=new Thread(task);
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- System.out.println(ReenterLock.i);
- }
- }
可重入锁除了以上加锁、解锁的基本功能外,还有其他一些功能:
lockInterruptibly方法和interrupt方法:后者在线程中已经出现过一次了,虽然名字一样,功能也差不多,但是作用对象不一样。如果我们的线程在加锁也就是获取锁时,用的是lockInterruptibly方法,如果在等待一段时间后,还没获取到锁,那么就可以通过interrupt方法通知这个线程不用等了。这两个方法配合使用,在设置合理的等待时间后,可以避免死锁的发生。但需要注意,被通知放弃获取锁的线程会释放自己的资源,结束执行任务。
tryLock方法:除了上面那种外部通知放弃获取锁的方法外,还有一种限时等待的方法,tryLock有两个参数,第一个是时间,第二个是时间类型。如果不传入任何参数,获取到锁直接返回true,没获取到直接返回false。对的,tryLock和普通的lock方法不同,它返回的是Boolean类型,所以一般需要配合if判断使用:
- @Override
- public void run() {
- try {
- if (lock.tryLock(10, TimeUnit.SECONDS)) {
- //逻辑代码
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
公平锁和非公平锁:公平锁的分配是公平的,先到先得。非公平锁则是随机分配锁的,你先等待的不一定能先获取到锁。具体的是在ReenterLock构造函数中进行设置:
- //构造函数传入true就是公平锁,默认情况下是非公平锁
- static ReentrantLock lock = new ReentrantLock(true);
默认情况下采用非公平锁,是因为公平锁需要维护一个有序队列,性能相较于非公平锁是非常低的。
Condition:可重入锁的搭档
在synchronized代码块中,可以使用wait方法让当前线程释放锁并等待,然后通过notify方法唤醒线程并尝试重新获取锁。但是这两个方法是作用在synchronized中的,前面也说过了。在可重入锁也有类似的功能,下面举个简单的例子,会发现和synchronized中的wait和notify差不都:
- public class Use {
- static ReentrantLock lock = new ReentrantLock();
- //创建的lock的condition对象
- static Condition condition = lock.newCondition();
- static int i = 0;
- static class Demo1 implements Runnable {
- @Override
- public void run() {
- //t1先进来加锁(遇到一次特殊情况,t2后启动的反而先获取到锁了)
- lock.lock();
- for (int j = 0; j < 10000; j++) {
- i++;
- if (i == 5000) {
- //1.输出5000
- System.out.println(i);
- try {
- //释放锁并等待
- condition.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- //被唤醒后接着执行完剩下的代码,并输出20000
- System.out.println(i);
- lock.unlock();
- }
- }
- static class Demo2 implements Runnable {
- @Override
- public void run() {
- //获取锁
- lock.lock();
- for (int j = 0; j < 10000; j++) {
- i++;
- }
- System.out.println(i);
- //2.执行完后输出15000,并唤醒等待的线程
- condition.signal();
- lock.unlock();
- }
- }
- public static void main(String[] args) throws InterruptedException {
- Demo1 task1 = new Demo1();
- Demo2 task2 = new Demo2();
- Thread thread1 = new Thread(task1);
- Thread thread2 = new Thread(task2);
- thread1.start();
- thread2.start();
- }
- }
await方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()方法或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法相似。
awaitUninterruptibly方法与await方法基本相同,但是它并不会在等待过程中响应中断。
singal方法用于唤醒一个在等待中的线程,singalAll方法会唤醒所有在等待中的线程。这和Obejct.notify()方法很类似。
Semaphore:允许多个线程同时访问
前面提到的可重入锁和同步代码块一次只能让一个线程进入,而Semaphore可以指定多个线程,同时访问一个资源。
- public class Use {
- static int i = 0;
- //一次允许两个线程进入
- static Semaphore sema = new Semaphore(2);
- static class Demo2 implements Runnable {
- @Override
- public void run() {
- try {
- //如果有多余的名额就允许一个线程进入
- sema.acquire();
- for(int j=0;j<10000;j++){
- i++;
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- //当前线程执行完代码并释放一个名额
- sema.release();
- }
- }
- }
- public static void main(String[] args) throws InterruptedException {
- //Demo1 task1 = new Demo1();
- Demo2 task2 = new Demo2();
- Thread thread1 = new Thread(task2);
- Thread thread2 = new Thread(task2);
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- System.out.println(i);//输出小于20000,说明同时有两个线程进去了,互相干扰了。
- }
- }
ReadWriteLock:读写锁
很多时候线程只是执行读操作,并不会互相干扰,其实这个时候并不需要线程之间相互排斥。在数据库里面读写锁是比较常见的,在Java中,它们的逻辑其实是一样的。只有读和读不会阻塞,有写操作必然阻塞。
代码篇幅太多了,就不再演示逻辑代码了,下面是读写锁的创建代码:
- static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
- //读锁
- static Lock readLock=readWriteLock.readLock();
- //写锁
- static Lock writLock=readWriteLock.writeLock();
CountDownLatch:倒计数器
一个实用的多线程工具类,说倒计数器可能有点不明白,其实就是等来指定数量的线程执行完后才执行接下来的代码,看示例更清楚:
- public class Use {
- //需要两个线程执行完任务
- static CountDownLatch count = new CountDownLatch(2);
- static int i=0;
- static class Demo2 implements Runnable {
- @Override
- public void run() {
- synchronized (Use.class) {
- for (int j = 0; j < 10000; j++) {
- i++;
- }
- //当前线程执行完任务,计数器+1
- count.countDown();
- }
- }
- }
- public static void main(String[] args) throws InterruptedException {
- //Demo1 task1 = new Demo1();
- Demo2 task2 = new Demo2();
- Thread thread1 = new Thread(task2);
- Thread thread2 = new Thread(task2);
- thread1.start();
- thread2.start();
- //等待指定数量的线程都执行完任务后才接着执行,相当于阻塞了当前的主线程,从而实现了join的功能
- count.await();
- System.out.println(i);//输出20000
- }
- }
Java并发编程:线程和锁的使用与解析的更多相关文章
- Java 并发编程 | 线程池详解
原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...
- java并发编程 线程基础
java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...
- Java并发编程:Concurrent锁机制解析
Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...
- Java并发编程:线程间通信wait、notify
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- java并发编程 | 线程详解
个人网站:https://chenmingyu.top/concurrent-thread/ 进程与线程 进程:操作系统在运行一个程序的时候就会为其创建一个进程(比如一个java程序),进程是资源分配 ...
- Java并发编程:线程和进程的创建(转)
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- Java并发编程——线程池的使用
在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统 ...
- Java并发编程——线程池
本文的目录大纲: 一.Java中的ThreadPoolExecutor类 二.深入剖析线程池实现原理 三.使用示例 四.如何合理配置线程池的大小 一.Java中的ThreadPoolExecutor类 ...
- Java并发编程中的锁
synchronized 使用synchronized实现同步有2种方式: 同步方法(静态与非静态) 同步代码块 任何Java对象均可作为锁使用,其中,使用的锁对象有以下3种: 静态同步方法中,锁是当 ...
- JAVA并发编程:相关概念及VOLATILE关键字解析
一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...
随机推荐
- C语言如何实现继承及容器
继承的概念 继承是面向对象软件技术当中的一个概念,与多态.封装共为面向对象的三个基本特征.继承可以使得子类具有父类的属性和方法或者重新定义,追加属性和方法. 面向对象中的重要概念就是类,在我们熟知的编 ...
- linux php 安装 openssl扩展
(1.生成 openssl.so 文件)#进入扩展目录cd /data/soft/php-5.5.38/ext/openssl#生成 configure 文件/usr/local/php/bin/ph ...
- Mac home 目录下创建文件夹
example:sudo vim /etc/auto_master before: # Automounter master map +auto_master # Use directory serv ...
- js事件冒泡于事件捕获
事件冒泡 事件捕获指的是从document到触发事件的那个节点,即自上而下的去触发事件. 事件冒泡是自下而上(从最深节点开始,向上传播事件)的触发事件 //例子 <div id="pa ...
- umditor删除域名,配置为绝对路径
getAllPic: function (sel, $w, editor) { var me = this, arr = [], $imgs = $(sel, $w); $.each($imgs, f ...
- 2019-2020-1 20199325《Linux内核原理与分析》第十二周作业
什么是ShellShock? Shellshock,又称Bashdoor,是在Unix中广泛使用的Bash shell中的一个安全漏洞,首次于2014年9月24日公开.许多互联网守护进程,如网页服务器 ...
- java中Locks的使用
文章目录 Lock和Synchronized Block的区别 Lock interface ReentrantLock ReentrantReadWriteLock StampedLock Cond ...
- mysql之浅谈主外键
主键(PRIMARY KEY) 主键在一个数据表中只能有唯一的一个,约束当前字段的值不能重复,且非空保证数据的完整性,也可以当做当前数据表的标识符用来查询(当做索引,唯一性索引的一种) 创建带主键的表 ...
- 使用3种协议搭建本地yum仓库
关闭防火墙和selinux [root@qls yum.repos.d]# systemctl stop firewalld (stop,start,disable,enable) [root@qls ...
- POJ2389 Bull Math【大数】
Bull Math Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 15040 Accepted: 7737 Descri ...