文章目录
前言
经常在Java岗位详情中有看到"熟悉io、多线程、集合等基础框架"的要求,而自己之前在学习SE部分的时候也只是简单的过了一遍,谈不上"熟悉"二字,所以趁着空闲的时候回过头来学习。以下内容是我在学习关于多线程内容时候的笔记总结,学习视频为b站up主 狂神说Java 的多线程详解 p1-p28。如有错误请指正,谢谢。
多线程编程
总而言之,多线程编程是为了满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
进程与线程
进程:操作系统管理的基本单元。进程包括由操作系统分配的内存空间,包含一个或多个线程。一个进程中可以并发多个线程,每条线程并行执行不同的任务。进程运行直到所有的非守护线程都结束运行后才能结束。
线程:进程中一个单一顺序的控制流,并且一个线程不能独立的存在,它必须是进程的一部分。
如下图活动监视器可以看到进程与线程:
线程创建
Java提供了三种创建线程的方法:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
(插一句嘴:之前在面试的时候也有想到过线程池算不算创建线程的一种方法,在对线程有了初步的了解之后个人认为是不算的。)
继承Thread类
自定义线程类继承Thread类,重写run()方法编写线程执行体,创建线程对象,调用start()方法启动线程。
TestThread.java
//总结:注意线程开启不一定立即执行而是有cpu调度执行
public class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("TestThread " + i);
}
}
public static void main(String[] args) {
//main线程
TestThread testThread = new TestThread();
testThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("mainThread " + i);
}
}
}
控制台输出:
而如果把testThread线程方法改成run()则先执行完testThread线程方法在执行剩下的方法。如下图诠释:
实现Runnable接口
实现Runnable接口具有多线程能力,启动线程:Thread对象.start(传入目标)。这样使用相对于继承Thread类避免OOP单继承局限性,灵活方便一个对象被多个线程使用。
MyRunnable.java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("TestThread " + i);
}
}
public static void main(String[] args) {
//main线程
TestThread testThread = new TestThread();
//创建线程对象,通过线程对象来开启线程,代理模式
new Thread(testThread).start();
for (int i = 0; i < 1000; i++) {
System.out.println("mainThread " + i);
}
}
}
实现Callable接口
实现Callbale接口需要返回值类型;重写call方法需要抛出异常。
- 创建目标对象
- 创建执行服务
- 提交执行
- 获取结果
- 关闭服务
MyCallable.java
public class MyCallable implements Callable<Boolean> {
@Override
public Boolean call() {
System.out.println("MyCallable call...");
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable testThread = new MyCallable();
ExecutorService service = Executors.newFixedThreadPool(1);
Future<Boolean> r = service.submit(testThread);
Boolean rs = r.get();
System.out.println(rs);
System.out.println("mainThread...");
service.shutdownNow();
}
}
线程状态
源码:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
生命周期
- 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。阻塞状态可以分为三种:等待阻塞(运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态)、同步阻塞(线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用))、其他阻塞(通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态)
- 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。线程中断或者结束,一旦进入死亡状态就不能再次启动。
停止线程
不推荐使用JDK提供的stop()、destroy()方法而且已废弃,建议使用一个标志为进行终止变量,当flag=false的时候则终止线程进行。
TestStop.java
public class TestStop implements Runnable{
//1.设置一个标志位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("thread run..." + i++);
}
}
//2.设置一个公开的方法停止线程,转换标志位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main thread..." + i);
if(i==900){
//调用stop方法切换标志位让线程停止
testStop.stop();
System.out.println("线程停止了...");
}
}
}
}
控制台输出:
线程休眠
sleep(时间)指定当前线程阻塞的毫秒数;sleep时间达到后线程进入就绪状态;sleep可以模拟网络延时,倒计时等;每个对象都有一个锁,而sleep不会释放锁。
案例:模拟倒计时
public class TestSleep2 {
public static void main(String[] args) {
try {
turnDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模拟倒计时
public static void turnDown() throws InterruptedException {
int num = 10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0){
break;
}
}
}
}
线程礼让
让当前正在执行的线程暂停,但不阻塞,从运行状态转为就绪状态,让cpu重新调度,礼让不一定成功。
TestYield.java
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}
线程强制执行
Join合并线程,待此线程执行完成后再执行其它线程,其它线程阻塞。
TestJoin.java
//线程强制执行相当于插队
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程VIP..."+i);
}
}
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 200; i++) {
if (i==100){
thread.join();
}
System.out.println("main "+i);
}
}
}
控制台输出:
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程优先级用数字来表示,范围从1~10。优先级高低只是意味着获得调度的概率高低,都是看CPU的调度。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
//更改优先级
public final void setPriority(int newPriority) {
//获得优先级
public final int getPriority() {
守护线程
线程分为用户线程和守护线程。虚拟机必须确保用户线程执行完毕;不用等待守护线程是否执行完毕。常见的守护线程如:后台操作记录日志、监控内存、垃圾回收等等。
案例:
//测试守护线程:上帝保佑
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
//默认是false表示用户线程,而正常的线程都是用户线程
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("God bless you");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("live...");
}
System.out.println("go die...");
}
}
线程同步
当多个线程访问、操作同一资源,并且某些线程还想修改这个对象,这时候就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进行这个对象的等待池形成队列,等待前面的线程使用完毕后下一个线程再使用。
初识线程并发问题
案例:简化版抢票
//多个线程同时操作同一对象
public class TestThread4 implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums <= 0){
break;
}
//模仿网络延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->抢到了第"+ ticketNums-- +"张票");
}
}
public static void main(String[] args) {
TestThread4 ticket = new TestThread4();
new Thread(ticket,"a").start();
new Thread(ticket,"b").start();
new Thread(ticket,"c").start();
}
}
控制台输出:
synchronized
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时也带来访问冲突问题,为了确保数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其它线程必须等待,使用后释放锁即可,但是也存在以下问题:
- 一个线程持有锁会导致其它所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
synchronized关键字包括两种用法:synchronized 方法和 synchronized 块。
案例:简化版优化抢票(synchronized 方法)
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"a").start();
new Thread(station,"b").start();
new Thread(station,"黄牛党").start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() throws InterruptedException {
if(ticketNums <= 0){
flag = false;
return;
}
//模拟延迟
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票");
}
}
同步块
同步块:synchronized (Obj){ }
Obj称之为同步监视器,Obj可以是任何对象,但是推荐使用共享资源作为同步监视器;同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身或者是class。
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码;
- 第二个线程访问,发现同步监视器被锁定,无法访问;
- 第一个线程访问完毕,解锁同步监视器;
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
当某个同步块中同时拥有"两个以上对象的锁"时,从而导致两个或多个线程都在等待对方释放资源都停止执行的"死锁"情况。
案例:灰姑娘和白雪公主去争夺口红和镜子-_-
public class DeadLock {
public static void main(String[] args) {
Makeup girl1 = new Makeup(0,"灰姑娘");
Makeup gril2 = new Makeup(1,"白雪公主");
girl1.start();
gril2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
class Makeup extends Thread{
//用static确保只有一份
static Lipstick lipstic = new Lipstick();
static Mirror mirror = new Mirror();
//选择
int choice;
//女孩名字
String girlName;
Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if(choice == 0){
synchronized (lipstic){
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的锁");
}
}
}else {
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstic){
System.out.println(this.girlName+"获得口红的锁");
}
}
}
}
}
控制台输出:程序无法正常停止,但是把程序中的两个synchronized块里面的另一个synchronized块拿出即可解决。
锁
Java提供更强大的线程同步机制:通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常见用的是 ReentrantLock,可以显式加锁、释放锁。
案例:简化版抢票
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//加锁
lock.lock();
if(ticketNums > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
synchronized与lock对比
- Lock是显式锁(手动去开启和关闭锁),synchronized是隐式锁,出了作用域自行释放。
- Lock只有代码块锁,而synchronized有代码块和方法锁。
- 使用Lock锁,JVM将花费较少时间来调度线程,性能更好,并且具有更好的扩展类(提供更多的子类)。
生产者消费者问题
生产者和消费者共享一个资源,并且生产者和消费者之间互相依赖也互为条件。对于生产者来说,没有生产之前要通知消费者等待;在生产之后又要通知消费者消费。对于消费者来说,在消费之后要通知生产者已经结束消费。
在生产者消费者问题中,仅有 synchnized 是不够的,是因为 synchnized可阻止并发更新同一个资源但是不能用来实现不同线程之间的信息传递。