java多线程 简书_Java多线程

一、基本概念

1、CPU核心数与线程数的关系

一般来说是1:1的关系 即1个核心对应1个线程,但我们在程序中可以创建多个线程的原因是由于CPU的时间片调度

2、CPU时间片轮转(RR调度)

把CPU的运行时间进行切片分别轮转到各个线程

3、进程和线程

进程:操作系统对资源分配的最小单位

线程:CPU调度的最小单位

进程>线程,线程不能单独存在,必须要依附于进程存在

线程数量限制:在操作系统层面Linux限制为1000,Windows限制为2000

4、并行和并发

(1)并行:同时执行(例如高速公路的4车道,并行数就是4)

(2) 并发:单位时间内的吞吐量(与并行的关键区别就在于时间限制),CPU的并发能力取决于CPU时间片的切换速度

二、多线程

1、线程的启动方式

Java中有三种线程启动的方式

(1)、继承Thread类

4357d769533c

image

(2)、实现Runnable接口

4357d769533c

image

(3)、实现Callable接口

4357d769533c

image

实现Runnable和Callable接口的不同在于,实现Callable是允许有返回值的;以上三种创建线程的方式,最后都是通过Thread类进行开启,在Java中只有Thread类是线程的创建和实现类。

2、线程的结束方式

(1)、stop方式

4357d769533c

image

stop方法被官方定义为弃用的;因为stop方法会强制退出线程,可能会导致线程中的其它资源未被正确释放等安全问题。

(2)、suspend方式

将线程挂起,同样也是被JDK弃用的;因为susbend方法不会释放锁,容易导致死锁发生

(3)、interrupt

Thread的成员方法,不会立刻导致线程退出,只会将线程的中断标志位置为true

(4)、interrupted

Thread的静态方法,返回boolean值,除具有isInterrupted正常功能外,还会重置中断标志位为false

(5)、isInterrupted

Thread的成员方法,判断线程是否被中断

一个线程退出的例子:

4357d769533c

image

调用interrupt方法将线程标志位置为true,用isInterrupted检测当前的线程标志位;当然在实际开发当中也可以通过一些boolean标志位进行控制。

3、线程的生命周期、线程调度、等待唤醒、ThreadLocal相关

(1)生命周期

线程的五种基本状态:新建、就绪、运行、阻塞、死亡

4357d769533c

image

当我们new一个线程并调用start方法时,该线程就从新建到就绪(Runnable)状态;当线程获取到CPU的时间片后进入到执行状态(Running);该线程调用yield方法或者时间片执行完毕后,从Running切换到Runnable状态;该线程调用wait(等待,释放锁)、sleep(休眠、不释放锁)、join(放弃执行让其他线程先执行)时会进入到阻塞(Blocked)状态;线程执行完毕后进入到死亡(Dead)状态。

(2)yield方法

使当前线程放弃时间片暂停执行,并执行其它线程;但由于CPU轮询速度较快,很可能马上又会轮询到当前线程,效果不明显

注:yield方法不会释放锁

(3)join方法

主要作用是线程的同步,使得线程从并行改为串行执行,例如线程A中执行线程B的join方法,表示线程B执行完成后才会继续执行线程A

eg:线程A、线程B、线程C三个线程,实现依次输出A、B、C中的内容

4357d769533c

image

4357d769533c

image

join方法的实现原理:

正常调用join方法实际上调用join(0)

public final void join() throws InterruptedException {

join(0);

}

join参数是一个delay时间,默认是0,但wait(0)不是等待0ms,而是一直等待。

void join():当前线程等该加入该线程后面,等待该线程终止。

void join(long millis):当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度。

void join(long millis,int nanos):等待该线程终止的时间最长为 millis 毫秒 + nanos纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度。

join的原理为当在线程A中调用了线程B的join方法,线程A会执行wait方法,释放锁并等待线程B的执行结束

public final void join(long millis) throws InterruptedException {

synchronized(lock) {

long base = System.currentTimeMillis();

long now = 0;

if (millis < 0) {

throw new IllegalArgumentException("timeout value is negative");

}

if (millis == 0) {

//传入的是0 默认执行这个判断

while (isAlive()) {//判断线程是否存活 native方法返回的boolean变量

lock.wait(0);//执行wait方法 释放锁 并等待

}

} else {

while (isAlive()) {

long delay = millis - now;

if (delay <= 0) {

break;

}

lock.wait(delay);

now = System.currentTimeMillis() - base;

}

}

}

}

(4)wait、notify、notifyAll方法

wait、notify、notifyAll被用于线程之间的协作,等待和唤醒;这几个方法都是Object而不是Thread的

注:为什么wait、notify、notifyAll要被设计成为Object下的方法而不是Thread的?

wait、notify、notifyAll使用的前提必须在同步代码块中,但Synchronized中的锁可以为任意对象,因此wait notify notifyAll放在所有类的父类Object中,方便管理。

wait用于线程的等待,与sleep的区别在于wait会释放锁,sleep不会释放锁

notify和notifyAll用于线程的唤醒,不会释放锁;notify用于唤醒一个线程,notifyAll会通知所有线程

等待通知的标准范式

1)等待方

a 获取对象锁(同步)

b 检查条件 条件不满足 wait

c 条件满足 执行业务代码

2)通知方

a 获取对象的锁(同步)

b 修改条件

c 通知等待方

以一个生产者消费者为例:

package com.hzf.demo;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.LinkedBlockingQueue;

public class MainTest {

public static void main(String[] args) throws InterruptedException,

ExecutionException {

LinkedBlockingQueue linkedBolckingQueue = new LinkedBlockingQueue<>(

10);

new ConsumerThread(linkedBolckingQueue).start();

new ProducterThread(linkedBolckingQueue, 10).start();

}

private static class ConsumerThread extends Thread {

private LinkedBlockingQueue mQueue;

private ConsumerThread(LinkedBlockingQueue queue) {

this.mQueue = queue;

}

@Override

public void run() {

super.run();

while (true) {

//获取同步锁

synchronized (mQueue) {

while (!mQueue.isEmpty()) {

System.out.println("消费了1个");

mQueue.remove();

mQueue.notify();

try {

sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

try {

mQueue.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

private static class ProducterThread extends Thread {

private LinkedBlockingQueue mQueue;

private int mMaxSize;

private ProducterThread(LinkedBlockingQueue queue, int maxSize) {

this.mQueue = queue;

this.mMaxSize = maxSize;

}

@Override

public void run() {

super.run();

while (true) {

//获取同步锁

synchronized (mQueue) {

while (mQueue.size() == mMaxSize) {

try {

mQueue.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

mQueue.add(1);

System.out.println("生产了1个");

// 如果生产量达到最大值 notify消费者消费

mQueue.notify();

try {

sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

}

(5)ThreadLocal的使用

4、Java线程锁的简单介绍

4357d769533c

Java锁分类.png

(1)可重入锁&不可重入锁

可重入锁:一个线程在外层方法中获取到了锁,进入内层方法后不需要再次获取锁

Synchronized和ReentrantLock都是可重入锁

(2)独享锁&共享锁

ReentrantLock是独享锁;ReentrantReadWriteLock是共享锁

独享锁也叫排它锁,指一个线程持有该锁之后,其它线程无法获取到该锁,持有锁的线程可以读、写数据

共享锁是指该锁可以被多个线程持有,以ReentrantReadWriteLock为例,当为读操作时,所有的线程都可以并发执行进行读操作,但无法修改数据;当为写操作时,所有其它的读写线程都会被排斥,无法进行读写操作。

(3)公平锁&非公平锁

ReentrantLock可以通过其构造方法指定当前锁为公平还是非公平锁

4357d769533c

ReentrantLock.png

公平锁指先申请的线程先拿到锁,按照申请顺序获取锁

非公平锁不一定按照申请的顺序获取锁

非公平锁的有点在于减少唤起线程的开销,整体的吞吐效率高,但处于等待的线程有可能会被饿死

(4)乐观锁&悲观锁

乐观锁和悲观锁是一种广义上的概念,乐观锁认为自己在操作数据的同时没有其它线程在修改数据,仅仅在更新数据的时候去判断有没有别的线程更改过当前数据(CAS算法,Java的原子类递增等操作都是通过CAS实现的);悲观锁认为自己在使用数据的同时一定会有别的线程修改数据,因此在获取数据的时候要先加锁,确保数据不会被其它线程锁修改,Java的Synchronized和Lock的实现类都是悲观锁。

(5)偏向锁、轻量级锁、重量级锁

偏向锁:是指同一段代码被一个线程多次获取,那么这个线程可以自动获取到锁,降低获取锁的代价;

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

4357d769533c

来自美团技术团队.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值