Java并发编程之Java线程


前言

记录一下Java并发编程的知识点。有部分内容是借鉴《Java并发编程的艺术》这本书的。本次先介绍一下线程。


01、线程简介

进程和线程的区别

  • 进程:当一个程序被运行,即把程序的代码从磁盘加载到内存,就是开启了一个进程。进程可以理解为程序的一个实例(例如打开网易云、浏览器)。

  • 线程:一个进程里面可以有多个线程,每个线程执行不同的任务(比如360安全卫士可以一边清理垃圾,一边扫描病毒),线程是现代操作系统调度的最小单位。

线程拥有各自的程序计数器,栈,并且可以共享堆里面的共享内存变量;

线程的状态

这里是从Java API层面上来讲的,先看一下Thread里面的枚举类

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED, 
    WAITING,   
    TIMED_WAITING,
    TERMINATED;
}

可以看出这里分成了六种状态:

  • NEW:线程刚被创建,还没有调用start方法
  • RUNNABLE:运行状态,涵盖了操作系统层面的就绪状态、运行状态、阻塞状态
  • BLOCKED:阻塞状态,表示线程阻塞于锁
  • WAITING:等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
  • TIMED_WAITING:超时等待状态,该状态不同于WAITING,它可以在指定的时间后自行返回
  • TERMINATED:终止状态,表示当前线程已经执行完毕

下图来自《Java并发编程的艺术》

在这里插入图片描述

线程创建的方式

1、继承Thread类

Thread t=new Thread(){
    @Override
    public void run() {
        System.out.println("创建了一个线程");
    }
};
//启动线程
t.start();

2、实现Runnable接口

Runnable t2= new Runnable() {
	@Override
	public void run(){
	// 要执行的任务
	}
};
// 创建线程对象
Thread t = new Thread(t2,"t2");
// 启动线程
t.start(); 

3、实现Callable接口(需要借助FutureTask来接收返回结果)

FutureTask<Integer> t3 = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return 404;
    }
});

new Thread(t3,"t").start();
//主线程阻塞,同步等待t3执行完毕返回结果
Integer result = t3.get();

4、使用线程池(第二部分详细介绍)

常见方法

方法名功能说明注意
start()启动一个线程,在新的线程运行run方法中的代码如果start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法
join()等待目标线程运行结束
getPriority()获取线程优先级
setPriority()设置线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记
isInterrupted()判断是否被打断不会清除打断标记
interrupted()判断当前线程是否被打断会清除打断标记
isAlive()线程是否存活
sleep(long n)让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
yield()提示线程调度器让出当前线程对CPU的使用

sleepyield的区别

sleep

  • 调用 sleep 会让当前线程从 RUNNABLE 进入 TIMED_WAITING 状态
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,而且会把中断标识位清除

yield

  • 调用 yield 会让当前线程让出CPU的使用权,注意当前线程会继续参与下一轮CPU使用权的争抢中,所以调用改方法后有可能还是当前线程继续运行

让线程“优雅”退出(线程中断)

中断可以理解为线程的一个标识位属性,它标识一个运行中的线程是否被其他线程进行了中断操作。其他线程可以通过调用该线程的interrupt方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,调用isInterrupted方法来进行判断是否被中断。

利用线程中断我们可以实现线程的“优雅”退出。代码如下

class TPTInterrupt {
    private Thread thread;
    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                //当我们调用stop对线程进行中断标记后这里就可以感知中断而结束
                if(current.isInterrupted()) {
                    log.debug("收拾东西走人");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("摸鱼ing......");
                } catch (InterruptedException e) {
                	//如果是在睡眠中被打断,会抛出异常,那么我们需要自己进行中断标记
                    current.interrupt();
                }
                
            }
        },"小明");
        thread.start();
    }
    public void stop() {
        thread.interrupt();
    }
}

在主方法调用

TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
System.out.println("你被辞退了");
t.stop();

运行结果

12:56:11.123 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:12.142 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:13.147 [小明] DEBUG xyx.product.web.TPTInterrupt - 摸鱼ing......
12:56:13.522 [main] DEBUG xyx.product.web.Test - 你被辞退了
12:56:13.522 [小明] DEBUG xyx.product.web.TPTInterrupt - 收拾东西走人

我们用一个图来看看执行流程

02、线程池

核心参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

主要有7大核心参数:

  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间
  • unit 时间单位
  • workQueue 阻塞队列
  • threadFactory 线程工厂
  • handler 拒绝策略

执行流程

核心线程数大小设置

  • CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io操作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了cpu资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N):系统会用大部分的时间来处理 I/O 操作,而线程等待 I/O 操作会被阻塞,释放 cpu资源,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N。

线程池的创建

使用ThreadPoolExecutor的构造方法创建线程池

ThreadPoolExecutor threadsPool = new ThreadPoolExecutor(9,
                20, 60,
                TimeUnit.SECONDS, new LinkedBlockingDeque<>(100),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

创建完线程池后有两种方式提交任务,分别是executesubmit方法

execute方法用于提交不需要返回值的任务

public void execute(Runnable command)

submit方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过future可以获取返回值,下面是它的三个接口

Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);

03、线程间通信

线程开始运行,拥有自己的栈空间,如果它们仅仅是孤立地运行,那么价值很少,如果多个线程之间可以相互配合完成工作,那么会带来更大的价值。

volatilesynchronized关键字

Java多线程访问共享变量时,每个线程都会有这个变量的拷贝副本,但是在执行过程中它可能看到的变量值不是最新的。

关键字volatile可以用来修饰共享变量,保证它对所有线程的“可见性”,即每个线程都可以读到最新值,详细原理我们下篇文章再介绍。

而关键字synchronized可以修饰方法或者同步代码块来确保同一个时刻,只能有一个线程处于方法或者同步块中,从而实现线程对共享变量访问的可见性和排他性

等待/通知机制

这个可以有3种实现方式,我们分别介绍一下:

1、Object类的wait和notify方法

我们先看一下下面表格了解一下方法
在这里插入图片描述
再加上下面这个例子理解一下

final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        synchronized (obj) {
            log.debug("执行....");
            try {
                obj.wait(); // 让线程在obj上一直等待下去
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("其它代码....");
        }
    }).start();
    new Thread(() -> {
        synchronized (obj) {
            log.debug("执行....");
            try {
                obj.wait(); // 让线程在obj上一直等待下去
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("其它代码....");
        }
    }).start();
    // 主线程两秒后执行
    sleep(2);
    log.debug("唤醒 obj 上其它线程");
    synchronized (obj) {
        obj.notify(); // 唤醒obj上一个线程
        sleep(1000);
        // obj.notifyAll(); // 唤醒obj上所有等待线程
    }
}

执行结果

09:36:09.796 [Thread-0] DEBUG xyx.product.web.Test - 执行....
09:36:09.799 [Thread-1] DEBUG xyx.product.web.Test - 执行....
09:36:10.797 [main] DEBUG xyx.product.web.Test - 唤醒 obj 上其它线程
09:36:11.801 [Thread-0] DEBUG xyx.product.web.Test - 其它代码....

注意事项

  • 使用上面的方法时需要获取到对象的锁
  • 使用wait方法后,线程状态由RUNNING变为WAITING,同时会释放对象锁
  • 调用notify或notifyAll方法后,需要等待notify或notifyAll的线程释放锁以后,等待线程才有机会从wait返回。
  • 从wait返回的前提是获取到了锁
  • 还有就是wait和notify的顺序不可以颠倒

2、Condition接口的await和signal方法

当我们调用Condition定义的方法时,需要获取到Condition对象关联的锁。而Condition对象是由Lock对象创建的,换句话说,Condition是依赖Lock对象的。

我们看一个例子(使用上和上面的第一种方法其实差不多)

class ConditionUseCase {
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.wait();
        } finally {
            lock.unlock();
        }

    }
    public void conditionSignal() {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

我们再来看看第二种方法对于第一种方法的提升,也可以理解为synchronized和ReentrantLock的一点区别:

  • 第一种方式在等待状态中不响应中断,第二种方式支持
  • 第一种方式不可以指定等待时间,第二种方式可以
  • 第一种方式只能随机唤醒或唤醒全部,第二种方式可以利用多个Condition实现指定目标唤醒

3、LockSupport工具类的park和unpark方法

LockSupport里面的方法提供了最基本的线程阻塞和唤醒功能。它的最大优势就是不需要先获取锁就可以直接使用,而且park和unpark方法无需顺序执行

我们直接看代码

Thread a = new Thread(() -> {
     log.debug("执行....");
     LockSupport.park();
     log.debug("其它代码....");
 });
 a.start();
 Thread b = new Thread(() -> {
     log.debug("执行....");
     //给指定线程放行
     LockSupport.unpark(a);
     log.debug("其它代码....");
 });
 b.start();

Thread.join() 方法

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止后才继续执行。我们还可以调用join(long millis)和join(long millis,int nacos)两个超时特性的方法。

我们可以看看下面的例子

Thread t1 = new Thread(() -> {
    try {
        sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    r1 = 10;
});
Thread t2 = new Thread(() -> {
    try {
        sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    r2 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);

运行结果

10:47:53.667 [main] DEBUG xyx.product.web.Test - r1: 10 r2: 10 cost: 3

总结

以上就是本篇文章的所有内容了,主要是对Java并发编程知识中的线程做一个介绍,后面再详细说一说底层的一些实现原理。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值