线程的使用

  新建线程

  新建一个线程有两种方法:继承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并发编程:线程和锁的使用与解析的更多相关文章

  1. Java 并发编程 | 线程池详解

    原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...

  2. java并发编程 线程基础

    java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...

  3. Java并发编程:Concurrent锁机制解析

    Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...

  4. Java并发编程:线程间通信wait、notify

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  5. java并发编程 | 线程详解

    个人网站:https://chenmingyu.top/concurrent-thread/ 进程与线程 进程:操作系统在运行一个程序的时候就会为其创建一个进程(比如一个java程序),进程是资源分配 ...

  6. Java并发编程:线程和进程的创建(转)

    Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...

  7. Java并发编程——线程池的使用

    在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统 ...

  8. Java并发编程——线程池

    本文的目录大纲: 一.Java中的ThreadPoolExecutor类 二.深入剖析线程池实现原理 三.使用示例 四.如何合理配置线程池的大小 一.Java中的ThreadPoolExecutor类 ...

  9. Java并发编程中的锁

    synchronized 使用synchronized实现同步有2种方式: 同步方法(静态与非静态) 同步代码块 任何Java对象均可作为锁使用,其中,使用的锁对象有以下3种: 静态同步方法中,锁是当 ...

  10. JAVA并发编程:相关概念及VOLATILE关键字解析

    一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...

随机推荐

  1. JasperReports入门教程(二):中文打印

    JasperReports入门教程(二):中文打印 背景 在上一篇中我们介绍了JasperReport的基本入门,也展示了一个报表.但是我们的示例都是使用的英文,如果我们把需要打印的数据改为中文会怎么 ...

  2. jeecg ant design vue一级菜单跳到外部页面——例如跳到百度

    需求:点击首页跳到百度新打开的页面 找到SideMenu.vue   对应的inde.js找到renderMenuItem 函数.加一个判断 if(menu.meta.url=='https://ww ...

  3. mycat入门部署安装

    mycat是一种比较简单的中间件产品,可以帮助mysql进行分库,同时统一在一个逻辑库. 硬件环境:系统:centos 7.6数据库版本:5.7.19mycat:1.6..6.1 github上下载m ...

  4. JS面向对象编程之对象

    在AJAX兴起以前,很多人写JS可以说都是毫无章法可言的,基本上是想到什么就写什么,就是一个接一个的函数function,遇到重复的还得copy,如果一不小心函数重名了,还真不知道从何开始查找错误,因 ...

  5. JAVA_WEB--jsp语法

    JSP声明 一个声明语句可以声明一个或多个变量.方法,供后面的Java代码使用.在JSP文件中,必须先声明这些变量和方法然后才能使用它们. JSP声明的语法格式: <%! declaration ...

  6. POJ 1170 Shopping Offers非状态压缩做法

    Shopping Offers Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 5659 Accepted: 2361 Descr ...

  7. 信息奥赛一本通1486: CH 6202 黑暗城堡 最短路径生成树计数

    1486:黑暗城堡 [题目描述] 知道黑暗城堡有 N 个房间,M 条可以制造的双向通道,以及每条通道的长度. 城堡是树形的并且满足下面的条件: 设 Di为如果所有的通道都被修建,第 i 号房间与第 1 ...

  8. 题解 bzoj 4398福慧双修(二进制分组)

    二进制分组,算个小技巧 bzoj 4398福慧双修 给一张图,同一条边不同方向权值不同,一条边只能走一次,求从1号点出发再回到1号点的最短路 一开始没注意一条边只能走一次这个限制,打了个从一号点相邻节 ...

  9. 使用 kind 快速搭建 kubernetes 环境

    使用 kind 快速搭建 Kubernetes 环境 Intro kind(Kubernetes IN Docker) 是一个基于 docker 构建 Kubernetes 集群的工具,非常适合用来在 ...

  10. 揭露.net培训结构软谋收钱踢学员的套路

    本人以下文章全部真实,希望管理员能通过,给更多的.net学者一个警示,避免更多的.neter掉入泥坑. 本人小码农一枚,主要做.net方向,苦于进步无门,各种资料收集渠道受限,最后狠心花一个月工资报名 ...