多线程入门

news2024/10/7 14:29:10

多线程简介

1. 进程/线程

# 进程
- 进程由指令和数据组成,指令要运行,数据要读写,必须将指令加载到CPU,数据加载到内存。指令运行过程中还需要用到磁盘,网络等IO设备
- 进程用来加载指令,管理内存,管理IO
- 一个程序被运行,从磁盘加载这个程序的代码到内存,就是开启了一个进程
- 进程可以视为一个程序的实例
- 大部分程序可以运行多个实例进程,也有的程序只能启动一个实例进程

# 线程
- 一个进程内部包含1-n个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- JVM中,进程作为资源分配的最小单元,线程作为最小调度单元

# 对比
- 进程基本上相互独立                                    线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间,供其内部的线程共享
- 进程间通信比较复杂: 同一台计算机的进程通信为IPC(Inter-process communication), 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HTTP
- 线程通信比较简单,因为它们共享进程内的内存,如多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般比进程上下文切换低

2. 并行/并发

2.1 并发

- 单核cpu下,线程是串行
- 任务调度器: 操作系统组件,将cpu的时间片(windows下为15ms)分给不同的线程使用
- cpu在线程间的切换非常快,感觉就是各个线程是同时运行的
- 微观串行,宏观并行

- concurrent:cpu核心同一个时间应对多个线程的能力

2.2 并行

- 多核cpu下,每个核都可以调度运行线程,这个时候线程就是并行的

- parrel: 同一个时间内,cpu真正去执行多个线程的能力
- 其实很多时候,并发和并行是同时存在的

在这里插入图片描述

3. 多线程应用场景

# 异步调用
- 某个方法的执行,不需要必须立刻获取返回结果
- 某个耗时业务,如果用另外一个线程来做,不会阻塞主线程的业务

# 提升效率
 - 一个任务,可以拆分为不同的任务,不同任务间互不依赖
- 多核cpu: 一个进程在进行多个独立操作时,没必要将其放到一个线程中顺序执行,创建多个线程分别执行,这样就会分到更多的cpu,执行更快
- 单核cpu: 没必要分成多个线程,因为依然会轮流执行,还会有上下文切换的损失
- java中要实现异步调用,必须采用多线程的方式

4. 创建线程

  • 启动JVM(main方法),就是开启了一个JVM进程
  • JVM进程内包含一个主线程,主线程可以派生出多个其他线程。同时还有一些守护线程,如垃圾回收线程
  • 主线程,守护线程,派生线程,cpu随机分配时间片,交替随机执行

4.1. 继承Thread类

  • 继承 Thread类,重写run(),start()启动线程
  • 两种写法:直接继承,匿名内部类
  • 在主线程内部开启了一个新的线程,在一个java进程中创造了一个其他线程
// 基本写法
package com.nike.erick.d01;

public class Demo01 {
    public static void main(String[] args) {
        ErickThread erickThread = new ErickThread();
        erickThread.start();

        System.out.println("Main Thread Running");
    }
}

class ErickThread extends Thread {

    @Override
    public void run() {
        System.out.println("Erick Thread Running");
    }
}

// 匿名内部类
package com.nike.erick.d01;

public class Demo02 {
    public static void main(String[] args) {
        Thread firstThread = new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ": Erick Thread");
            }
        };

        firstThread.start();
        System.out.println("Erick Main Running");
    }
}

4.2 实现Runnable接口

  • 将要启动的线程和要执行的任务分开,更加灵活
  • 实现Runnable接口,并将该对象作为Thread构造方法的参数传递
  • 三种方式: 直接实现,匿名内部类,lambda
- Runnable把线程和任务分开了
- Runnable更加容易和线程池相关的API结合使用
- Runnable让任务脱离了继承体系,更加灵活
- Runnable避免单继承的问题
// 实现Runnable接口
package com.nike.erick.d01;

public class Demo03 {
    public static void main(String[] args) {
        Thread firstThread = new Thread(new LucyThread(), "t1");
        firstThread.start();
        System.out.println("Main Thread Running");
    }
}

class LucyThread implements Runnable {

    @Override
    public void run() {
        System.out.println("Lucy Thread Running");
    }
}
// 匿名内部类
package com.nike.erick.d01;

public class Demo04 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Lucy Thread");
            }
        }, "t1");

        thread.start();

        System.out.println("Main Thread");
    }
}
// Lambda 方法
package com.nike.erick.d01;

public class Demo04 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("Lucy Thread"), "t1");

        thread.start();

        System.out.println("Main Thread");
    }
}

Runnable vs Thread

Runnable

# 策略模式
- 实际执行时候是调用的Runnable接口的run方法
- 因为将Runnable实现类传递到了Thread的构造参数里面
  • Runnable接口
// @FunctionalInterface修饰的接口,可以用lambda来创建
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • Thread 类
public class Thread implements Runnable {

    private Runnable target;
    
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

Thread

  • Thread的实现类重写了run方法,因此在通过Thread的start方法调用的时候,实际是调用了实现类的run方法

4.3 FutureTask接口

  • 可以获取任务执行结果的一个接口
# FutureTask 继承关系
class FutureTask<V> implements RunnableFuture<V>
interface RunnableFuture<V> extends Runnable, Future<V>
interface Future<V> :
                       boolean cancel()
                       boolean isCancelled()
                       boolean isDone()
                       V get()
                       V get(long timeout, TimeUnit unit)

# Callable 接口实现类
interface Callable<V>
package com.nike.erick.d01;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class Demo06 {
    public static void main(String[] args) {

        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("slave-thread running");
                TimeUnit.SECONDS.sleep(2);
                return "success from erick";
            }
        });

        Thread thread = new Thread(futureTask, "erick-thread");
        thread.start();

        try {
            /*获取结果的时候,会将主线程阻塞*/
            System.out.println("slave-thread result: " + futureTask.get());
            System.out.println("slave-thread result: " + futureTask.isCancelled());
            System.out.println("slave-thread result: " + futureTask.isDone());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("Main Thread ending");
    }
}

5. 进程查看

5.1. Linux

# process status
ps -ef
ps -fe               # 进程查看

ps -ef|grep java
ps -fe|grep java     # 管道运算符, 查看java进程(包含java进程和查看的grep进程)

kill pid             # 杀死指定进程
top                  # 动态查看当前进程的占用cpu和mem情况。ctrl+c 退出

top -H -P (pid)      # 查看某个进程内部的所有线程的cpu使用
                     # -H : 表示查看线程

5.2 Java

JPS

5.3 线程工具JConsole

  • Java内置,检测java线程的图形化界面工具
  • 位于jdk/bin/目录下
  • 直接在Mac控制台输入: jconsole即可打开
  • 可以用来检测死锁

6. 主线程和守护线程

  • main方法启动后,就会开启一个JVM, 包含一个主线程 和 守护线程, 其他线程
  • JVM就是一个java进程
  • 守护线程: 只要其他非守护线程运行结束了,即使守护线程的代码没执行完,也会强制退出
  • 垃圾回收器就是一种守护线程
package com.nike.erick.d01;

import java.util.concurrent.TimeUnit;

/* 输出结果: Dame Starting
            Main Thread ending*/
public class Demo08 {
    public static void main(String[] args) throws InterruptedException {
        Thread daemonThread = new Thread(() -> {
            try {
                System.out.println("Dame Starting");
                TimeUnit.SECONDS.sleep(10);
                System.out.println("Dame");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        daemonThread.setDaemon(true);
        daemonThread.start();

        TimeUnit.SECONDS.sleep(2);
        System.out.println("Main Thread ending");
    }
}

7. JVM 模型

7.1 栈与栈帧

  • Java Virtual Machine Stacks: Java虚拟机栈内存
  • 栈内存:虚拟机启动后,每个线程启动后,都会分配一块独立的栈内存
  • 栈帧: 每个栈包含多个栈帧(Frames),对应着每次方法调用时所占的内存
  • 活动栈帧:每个线程只能有一个活动栈帧,对应着当前正在执行的方法
  • 弹栈:方法执行完毕后,栈帧内存依次弹栈,栈内存销毁

在这里插入图片描述

7.2 Thread Context Switch

  • 因为一些原因导致cpu不再执行当前线程,转而执行另一个线程代码
1. 线程的cpu时间片用完
2. 垃圾回收:               # 垃圾回收的时候,其他所有线程要暂停, STW
3. 有更高优先级的线程需要运行
4. 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
  • 当发生线程上下文切换时候,需要操作系统保存当前线程的状态,并恢复另一个线程的状态
  • 程序计数器:java中的, 线程切换时,记住下一条jvm指令的执行地址,是线程私有的
  • 状态包含:程序计数器,虚拟机中每个栈帧的信息,操作数栈,返回地址等
  • Thread Context Switch频繁发生会影响性能

线程方法

1. start/run

public synchronized void start()
public void run()

1. start() :  
       1.1 线程从new状态转换为runnable状态,等待cpu分片从而有机会转换到running状态
       1.2 在主线程之外,再开启了一个线程
       1.3 已经start的线程,不能再次start  “IllegalThreadStateException”
       
2. run():    
      2.1 线程内部调用start后实际执行的一个普通方法
      2.2 如果线程直接调用run() ,只是在主线程内,调用了一个普通的方法

2. sleep/yield

2.1 sleep

public static native void sleep(long millis) throws InterruptedException

1. 线程放弃cpu,从RUNNABLE 进入 TIMED_WAITING状态
2. 睡眠中的线程可以自己去打断自己的休眠
3. 不会放弃当前的锁资源
4. 睡眠结束后,会变为RUNNABLE,并不会立即执行,而是等待cpu时间片的分配
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("slave-thread running");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("slave-thread before start: " + thread.getState()); //NEW

        thread.start();
        System.out.println("slave-thread after start: " + thread.getState()); // RUNNABLE

        TimeUnit.SECONDS.sleep(2);
        System.out.println("slave-thread while sleeping: " + thread.getState()); // TIMED_WAITING
    }
}
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("slave-thread running");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    System.out.println("slave-thread interrupted");
                    throw new RuntimeException("Interrupted Exception");
                }
                System.out.println("slave-thread ending"); // 不会执行
            }
        });

        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt(); // 打断睡眠中的线程
    }
}

2.2 yield

public static native void yield()

- 线程让出cpu资源,让其他线程先去执行
- 让线程从RUNNING状态转换为RUNNABLE状态
- 假如其他线程不用cpu,那么cpu又会分配时间片到当前线程,可能压根就没停下

2.3 区别

sleep: 让当前线程从 RUNNING --> Timed Waiting(阻塞), 睡眠结束后会进入 RUNNABLE状态
yield: 让当前线程从 RUNNING --> RUNNABLE 状态,然后调度其他线程

priority及yield

package com.nike.erick.d02;

public class Demo03 {
    public static void main(String[] args) {
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (true) {
                    i++;
                    System.out.println("T1--->" + i);
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (true) {
                Thread.yield();// 可以放弃当前线程
                    i++;
                    System.out.println("        T2--->" + i);
                }
            }
        });

        firstThread.setPriority(10);
        firstThread.start();
        secondThread.setPriority(1);
        secondThread.start();
    }
}

sleep应用

  • 程序一直执行,浪费cpu资源
package com.dreamer.multithread.day02;

public class Demo01 {
    public static void main(String[] args) {
        while (true) {
            System.out.println("i am working");
        }
    }
}
  • 程序间歇性休眠,让出cpu资源, 避免空转
package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("i am working");
        }
    }
}

3. Join

  • 谁调用 join,就等谁的线程结束后再去运行当前线程

3.1. join()

  • 等待当前线程执行完毕
public final void join() throws InterruptedException

单个线程

package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    static int number = 0;

    public static void main(String[] args) {
        Thread slaveThread = new Thread("t1") {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                number = 10;
            }
        };

        slaveThread.start();
        
        // t1线程调用join,t1线程就是插队执行
        try {
            slaveThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("number: " + number);
    }
}

多个线程

package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo03 {
    public static void main(String[] args) throws InterruptedException {
        Thread firstThread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread secondThread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        firstThread.start();
        secondThread.start();

        /**
         * 1. 两个线程同时插队,以相同优先级执行
         * 2. 所以一共等待时间为2s
         */
        long begin = System.currentTimeMillis();
        firstThread.join();
        secondThread.join();
        long end = System.currentTimeMillis();

        System.out.println("total time consuming: " + (end - begin));
    }
}

3.2 join(long millis)

  • 有时效的等待:最多等待多少ms, 0 代表永远执行完毕
  • 假如线程join的等待时间超过了实际执行时间,执行完后就可以不用继续等了
public final synchronized void join(long millis) throws InterruptedException
package com.dreamer.multithread.day02;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    static int number = 0;

    public static void main(String[] args) {
        Thread slaveThread = new Thread("t1") {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                number = 10;
            }
        };

        slaveThread.start();

        // t1线程调用join,t1最多插队1秒,然后就继续执行当前线程
        try {
            slaveThread.join(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 0
        System.out.println("number: " + number);
    }
}

3.3 应用

  • 同步等待其他线程的结果调用

4. interrupt

# Thread 类

# 1. 打断线程,谁调用打断谁
public void interrupt()

# 2. 判断线程是否被打断 : 默认值为true
         #  不会清除打断标记
public boolean isInterrupted()

         #  会将打断标记置为false
public static boolean interrupted()

4.1 打断阻塞线程

  • 打断线程,抛出异常,并将线程打断标记重置为false(需要一点缓冲时间)
  • 如sleep,join,wait的线程,被打断的线程抛出错误
package com.erick.multithread.d1;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread slaveThread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        slaveThread.start();

        TimeUnit.SECONDS.sleep(3);

        // false
        System.out.println(slaveThread.isInterrupted());
        slaveThread.interrupt();
        // 留下一点缓冲时间
        TimeUnit.SECONDS.sleep(3);
        // 阻塞的线程,被打断后,后通过异常的方式抛出,并将打断标记重制为false
        // false
        System.out.println(slaveThread.isInterrupted());
    }
}

4. 2 打断正常线程

普通打断

  • 正常线程运行时,收到打断, 打断信号变为true,但不会做任何处理
  • 并不会直接终止打断线程,而是发出打断线程的请求
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        Thread slaveThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("slave-thread running");
                }
            }
        });

        slaveThread.start();
        TimeUnit.SECONDS.sleep(1);

        /*正常运行的线程,被打断后
         * 1. 继续正常运行
         * 2. 将打断标记置为 true*/
        slaveThread.interrupt();
    }
}

通过打断标记

package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        Thread slaveThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    /*判断打断标记*/
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                    System.out.println("slave-thread running");
                }
            }
        });

        slaveThread.start();
        TimeUnit.SECONDS.sleep(1);

        /*正常运行的线程,被打断后
         * 1. 继续正常运行
         * 2. 将打断标记置为 true*/
        slaveThread.interrupt();
    }
}

Two Phase Termination

介绍

  • 终止正常执行的线程的优雅的模式:留下料理后事的机会
# 1. 错误思路一:
stop()   已经被废弃
   stop会真正kill线程,如果这时线程锁住了共享资源,
    那么当该线程被杀死后,再也没有机会去释放锁,其他线程将永远无法获取锁
# 2. 错误思路二: 
System.exit()
   目的仅仅是停止一个线程,但这种做法会将当前线程所在的整个java程序终止

业务场景

  • 监视线程,每隔2秒去执行监视流程,如果被打断,则中止监视流程
package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;

public class Demo06 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        // 料理后事
                        closeJob();
                        break;
                    }

                    /*阶段一: 业务代码: 可能被打断*/
                    try {
                        executeBusiness();
                    } catch (Exception e) {
                        continue;
                    }

                    /*阶段二:休眠操作: 也可能被打断*/
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        System.out.println("休眠时候被打断了: " + Thread.currentThread().isInterrupted());
                        // 如果休眠时被打断了,那么打断标记就变为了false, 需要再次打断,重制标记为为true
                        // 这里就类似于打断正常线程
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });

        thread.start();

        TimeUnit.SECONDS.sleep(4);
        thread.interrupt();
    }

    private static void executeBusiness() {
        System.out.println("执行了业务");
    }

    private static void closeJob() {
        System.out.println("料理后事");
    }
}

4.3 打断park线程

  • LockSupport: public static void park()
  • park当前的线程: 线程一直生存,直到被打断才会继续往下执行
  • 被打断后,打断标记就会变为true,就不能二次park了

单次park

package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class Demo07 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread start running....");

                // 将当前线程停下来
                LockSupport.park();
                System.out.println("after first park...");
            }
        });

        thread.start();
        TimeUnit.SECONDS.sleep(2);
        // 打断后就会继续执行
        thread.interrupt();
    }
}

多次park

package com.nike.erick.d02;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class Demo08 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("slave thread running");
                LockSupport.park();
                System.out.println("after first park...");

                // 获取当前线程的打断标记,同时将打断标记清除,即为 false
                System.out.println("打断标记:" + Thread.interrupted());

                LockSupport.park(); // 再次park
                System.out.println("after second park...");
            }
        });

        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        TimeUnit.SECONDS.sleep(3);
        thread.interrupt();
    }
}

5. 其他方法

### 1. 成员方法

# 1. 名字
public final synchronized void setName(String name)  
public final String getName()

# 2. 优先级:最小为1,最大为10,默认为5
#    只是一个cpu执行目标线程的顺序  建议设置,任务调度器可以忽略它进行分配资源
public final void setPriority(int newPriority)
public final int getPriority()

# 3. 线程id: 13
public long getId()

# 4. 是否存活
public final native boolean isAlive()

# 5. 是否后台线程
public final boolean isDaemon()

# 6. 获取线程的状态
public State getState()
NEW  RUNNABLE  BLOCKED  WAITING   TIMED_WAITING   TERMINATED


### 2. 静态方法
# 1. 获取当前线程
public static native Thread currentThread()


## 3. 过时方法
- stop:停止线程运行
- suspend: 让线程暂停使用
- resume: 恢复线程运行
- 不推荐理由: 这三种方法,都可能造成死锁问题

生命周期

1. 操作系统

  • 从操作系统层面来说,包含五种状态
  • cpu的时间片只会分给可运行状态的线程,阻塞状态的需要其本身阻塞结束

image-20220925095436125

NEW

  • new 出一个Thread的对象时,线程并没有创建,只是一个普通的对象
  • 调用start,new 状态进入runnable状态

RUNNABLE

  • 调用start后,在jvm中创建了新的线程,但并不立即执行,只是处于就绪状态,即有资格执行
  • 等待cpu分配权限,只有轮到它的时候,才会真正执行

RUNNING

  • 一旦cpu调度到了该线程,该线程才会真正开始执行
# 该状态的线程可以转换为其他状态
1. 进入TERMINATED:   stop(不推荐), 线程执行完毕, jvm crash
2. 进入BLOCK:        sleep, wait,阻塞IO如网络数据读写, 获取某个锁资源
3. 进入RUNNABLE:     cpu轮询到其他线程,线程主动yield放弃cpu

BLOCK

# 该状态的线程可以转换为其他状态
1. 进入TERMINATED:   stop(不推荐), jvm crash
2. 进入RUNNABLE:     线程阻塞结束,完成了指定时间的休眠
                     wait中的线程被其他线程notify/notifyall
                     获取到了锁资源

TERMINATED

  • 线程正常结束
  • 线程运行出错意外结束
  • jvm crash,导致所有的线程都结束

2. JAVA层面

  • 根据Thread类中内部State枚举,分为六种状态

线程安全

1. 线程不安全

1.1 共享变量

  • 多个线程对共享变量的并发修改,导致结果并不像预期中那样
package com.nike.erick.d03;

public class Demo01 {
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    number++;
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    number--;
                }
            }
        });

        firstThread.start();
        secondThread.start();

        firstThread.join();
        secondThread.join();

        System.out.println("number: " + number);
    }
}

1.2 原因

字节码指令

  • 线程拥有自己的栈内存,读数据时会从主内存中拿,写完后会将数据写回主存
  • i++在实际执行的时候,是一系列指令,一系列指令就会导致指令交错

在这里插入图片描述

指令交错

  • 指令交错存在于多线程之间
  • 线程上下文切换,引发不同线程内指令交错,最终导致上述操作结果不会为0

在这里插入图片描述

概念

  • 线程不安全: 只会存在于多个线程共享的资源
  • 临界区: 对共享资源的多线程读写操作的代码块
  • 竞态条件: 多个线程在在临界区内,由于代码的指令交错,对共享变量资源的争抢
多线程  读    共享资源  没问题
多线程  读写  共享资源  可能线程不安全(指令交错)

2. Synchronized

  • 一种阻塞式解决线程安全问题的方案

2.1 同步代码块

基本语法

  • 对象锁,只要为同一个对象,为任意对象(除基本类型)
  • 对象锁:尽可能用final修饰,这样保证对象的引用不可变,从而确保是同一把锁
- synchronized: 一种阻塞式的,用来解决多线程访问共享资源引发的不安全的解决方案
- synchronized: 可在不同代码粒度进行控制
- synchronized: 保证了《临界区代码的原子性(字节码)》,不会因为线程的上下文切换而被打断
- synchronized: 必须保证对对同一个对象加锁(Integer.value(0))
package com.nike.erick.d03;

public class Demo01 {
    private static int number = 0;

    /*任何对象都可以,只要保证是引用类型*/
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        /*粗粒度的锁
          细粒度的锁:也可以加在每个for循环中*/
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 100000; i++) {
                        number++;
                    }
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 100000; i++) {
                        number--;
                    }
                }
            }
        });

        firstThread.start();
        secondThread.start();

        firstThread.join();
        secondThread.join();

        System.out.println("number: " + number);
    }
}

原理

1. a线程获取锁,执行代码
2. 其他线程这个时候要进入,无法获取锁资源,就会被block,进入  《等待队列》
   同时进入上下文切换
   
3. a线程执行完毕后,释放锁资源。唤醒其他线程,进行cpu的争夺

面向对象改进

package com.nike.erick.d03;

public class Demo03 {
    private static Room room = new Room();

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

        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    room.decrement();
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    room.increment();
                }
            }
        });

        firstThread.start();
        secondThread.start();
        firstThread.join();
        secondThread.join();

        System.out.println("result:" + room.getValue());
    }
}

class Room {
    private int counter;

    // 锁对象一般用当前对象
    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }

    public int getValue() {
        synchronized (this) {
            return counter;
        }
    }
}

2.2 同步方法

成员方法

  • 同步成员方法和同步代码块效果一样,必须保证同步代码块的锁对象是this对象
  • 可能锁粒度不太一样
  • 同步方法的锁对象是this,即当前对象
@Data
class Calculator {
    private int number;

    public void incr() {
        synchronized (this) {
            for (int i = 0; i < 10000; i++) {
                number++;
            }
        }
    }
    // 同步方法
    public synchronized void decr() {
        for (int i = 0; i < 10000; i++) {
            number--;
        }
    }
}

静态成员方法

  • 锁对象:锁用的是类的字节码对象: Calculator.class
@Data
class Calculator {
    private static int number;

    public int getNumber() {
        return number;
    }

    public static void incr() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Calculator.class) {
                number++;
            }
        }
    }

    public static synchronized void decr() {
        for (int i = 0; i < 10000; i++) {
            number--;
        }
    }
}

锁对象

- 同步代码块,必须保证用的锁是同一个对象,但是不能为基本数据类型
- 同步成员方法的锁对象是this
- 同步静态成员方法的锁对象是当前类的字节码对象     .class
- 如果多个线程对共享变量读写,但是部分线程没有加锁保护,依然线程不安全

3. 线程安全场景

3.1 成员变量/静态成员变量

1. 没被多线程共享:        则线程安全
2. 被多线程共享:
     2.1 如果只读,   则线程安全
     2.2 如果有读写, 则可能发生线程不安全问题

3.2 局部变量

线程安全

# 每个线程的方法都会创建单独的栈内存,局部变量保存在自己当前方法的栈桢内
# 局部变量线程私有

1. 局部变量是基础数据类型时: 是线程安全的
2. 但局部变量是应用类型时:   可能不安全
  2.1 如果该对象没有逃离方法的作用访问,则线程安全
  2.2 如果该对象逃离方法的作用范围,则可能线程不安全 《引用逃离》
 
# 避免线程安全类变为不安全类: 不要让一个类的方法被重写
- final修饰禁止继承,或对可能引起安全的方法加private

引用逃离

  • 如果一个类不是final类,那么就可能被继承
  • 被继承的时候发生方法覆盖,覆盖方法如果创建新的线程,就可能发生局部变量不安全
// 安全
package com.nike.erick.d01;

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        SafeCounter safeCounter = new SafeCounter();
        safeCounter.operation(list);
        System.out.println(list);
    }
}

class SafeCounter {
    public void operation(List<String> list) {
        for (int i = 0; i < 10000; i++) {
            addElement(list);
            deleteElement(list);
        }
    }

    public void addElement(List<String> list) {
        list.add("HELLO");
    }

    public void deleteElement(List<String> list) {
        list.remove(0); // 移除元素
    }
}
// 不安全
package com.nike.erick.d01;

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        UnsafeCounter unsafeCounter = new UnsafeCounter();
        unsafeCounter.operation(list);
        System.out.println(list);
    }
}

class SafeCounter {
    public void operation(List<String> list) {
        for (int i = 0; i < 10000; i++) {
            addElement(list);
            deleteElement(list);
        }
    }

    public void addElement(List<String> list) {
        list.add("HELLO");
    }

    public void deleteElement(List<String> list) {
        list.remove(0); // 移除元素
    }
}

class UnsafeCounter extends SafeCounter {
    @Override
    public void deleteElement(List<String> list) {
        /*开启了新的线程来改变*/
        // index out of bound
        // 相当于把删除的操作延迟或提前了
        new Thread(() -> list.remove(0)).start();
    }
}

3.3 线程安全类

JDK类

  • 多个线程同时调用他们同一个实例的方法时,线程安全
  • 线程安全类中的方法的组合,不一定线程安全
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
package com.nike.erick.d03;

import java.util.Hashtable;
import java.util.concurrent.TimeUnit;

public class Demo04 {

    /*共享资源*/
    private static Hashtable<String, String> hashtable = new Hashtable<>();

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

        Thread firstThread = new Thread(() -> combinedMethod(hashtable));

        Thread secondThread = new Thread(() -> combinedMethod(hashtable));

        firstThread.start();
        secondThread.start();
        firstThread.join();
        secondThread.join();
        System.out.println(hashtable.size());
    }

    // 方法的组合不是 线程安全的
    // 如果要线程安全,必须将组合方法也设置成 synchronized
    public static void combinedMethod(Hashtable<String, String> hashtable) {
        if (null == hashtable.get("name")) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            hashtable.put("name", "erick");
            System.out.println(Thread.currentThread().getName());
            hashtable.put(Thread.currentThread().getName(), "00");
        }
    }
}

不可变类

  • 类中属性都是final,不能修改
  • 如 String,Integer
#  实现线程安全类的问题
- 无共享变量
- 共享变量不可变
- synchronized互斥修饰

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/19888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CentOS7 安装rabbitMQ步骤

全程使用root权限 1、修改主机名称 hostnamectl set-hostname rmq158 2、输入命令&#xff1a;vi /etc/hosts修改hosts文件 3、重启 4、WINSCP传输两个包到/usr/local下面 5、tar -xvf otp_src_21.3.tar.gz cd otp_src_21.3 ./configure --prefix/usr/local/erlang 6、yum i…

教学:制作 GitHub 同步近期博客卡片

这几天看到有小伙伴将自己近期更新的博客同步显示到了 GitHub 主页&#xff0c;这么有趣的小卡片我是一定要尝试一把的&#xff0c;完整的教程我已经整理好了&#xff0c;一起搞起来吧~ 2. 开始教程 2.1 实现流程&#xff1a; Github 的主页装修主要讲的就是主页的 Markdown 文…

Spring Security自定义验证码登录

本文内容来自王松老师的《深入浅出Spring Security》&#xff0c;自己在学习的时候为了加深理解顺手抄录的&#xff0c;有时候还会写一些自己的想法。 验证码登录也是项目中一个常见的需求&#xff0c;但是Spring Security并未提供自动化配置方案。所以需要开发者自行定义。这里…

Spring的创建和使用

1. 创建Spring项目 1.1 创建一个 Maven 项目 不需要使用任何模板, 点击下一步.注:所有的名称都不能包含中文.Maven仓库中的结构: 1.2 添加 Spring 框架支持 在Spring 的 配置文件 中添加依赖 spring-context: Spring的上下文spring-beans: 管理Spring对象(bean) <depende…

36、异常(Exception)

一、 引入: 不应该出现了一个不算致命的问题就导致整个系统崩溃&#xff0c;所以java设计者提供了一个异常处理机制来解决问题 快速入门&#xff1a; package exception_;public class Exception01 {public static void main(String[] args) {int num110;int num20;//异常处…

软件工程复习

文章目录&#xff08;一&#xff09;软件软件发展三阶段软件的概念什么是软件危机&#xff1f;内容包括&#xff1a;软件危机的表现&#xff1a;软件危机的原因(4)消除软件危机的途径&#xff1a;软件工程软件工程定义&#xff08;要背&#xff09;简称&#xff1a;软件工程的基…

【Java基础】第六章 | IO流

目录 | 总框架 | 文件路径相关知识 | Peoperties类与IO流、配置文件* | 1.文件流 文件字节输入流&#xff08;标准输入流&#xff09; FileInputStream 文件字节输出流 FileOutputStream 文件字符输入流 FileReader 文件字符输出流 FileWriter 应用&#xff1a;使用字…

MMDetection库中的一些模块介绍

本文目前仅包含1个主干网络和1个颈部网络。如果有机会&#xff0c;会继续补充更多模型。 若发现内容有误&#xff0c;欢迎指出。 MMDetection的图像数据一般会经历如下步骤/模块&#xff1a; #mermaid-svg-XxM18Ychr9OSpdV6 {font-family:"trebuchet ms",verdana,ari…

JavaScript 防抖与节流

目录1 函数1.1 调用函数1.2 闭包2 防抖与节流2.1 定义2.2 区别2.3 应用场景3 防抖3.1 非立即执行3.1.1 一般写法3.1.2 Vue2 中写法3.1.3 过程3.2 立即执行3.2.1 一般写法3.2.2 Vue2 中写法3.2.3 过程1 函数 应用防抖节流首先需理解以下知识 1.1 调用函数 js 函数内部 return…

电影售票系统

项目介绍 基于SpringBoot &#xff0c;Mybatis&#xff0c; Vue 的电影售票及影院管理系统&#xff08;前后端分离&#xff09;&#xff0c;具体功能见 下面演示截图 需要安装的软件 Java8 MySQL5.7或以上 Navicat或者其他管理工具 IDEA或者Eclipse Node.js 14或以上 运行项…

PLC学习笔记(三):PLC结构(2)

目录&#xff1a; PLC学习笔记&#xff08;一&#xff09;&#xff1a;概述 PLC学习笔记&#xff08;二&#xff09;&#xff1a;PLC结构&#xff08;1&#xff09; PLC学习笔记&#xff08;三&#xff09;&#xff1a;PLC结构&#xff08;2&#xff09; &#x1f981;&…

SpringBoot+Mybaits搭建通用管理系统实例八:系统权限控制实现

一、本章内容 实现自定义权限控制,通过自定义PermissionEvaluator实现操作权限的检测及控制,关于权限控制模型有ACL, DAC, MAC, RBAC, ABAC等,具体原理可参考:【权限系统设计】ACL, DAC, MAC, RBAC, ABAC 模型的不同应用场景 完整课程地址 二、开发视频 SpringBoot+Mybaits…

《操作系统-真象还原》12. 进一步完善内核

文章目录Linux 的系统调用系统调用的实现 —— 图解系统调用的实现 —— 代码触发中断寻找 IDT 中断描述符执行对应的中断例程中断例程中通过用户传入的功能号去执行对应的功能函数关于 printf你需要知道可变参数的原理Linux 中的可变参数原理Linux 中的可变参数实现printf 只是…

【微服务】SpringCloud轮询拉取注册表及服务发现源码解析

&#x1f496; Spring家族及微服务系列文章 ✨【微服务】SpringCloud微服务剔除下线源码解析 ✨【微服务】SpringCloud微服务续约源码解析 ✨【微服务】SpringCloud微服务注册源码解析 ✨【微服务】Nacos2.x服务发现&#xff1f;RPC调用&#xff1f;重试机制&#xff1f; ✨【微…

机器学习:支持向量机SVM的SVC和SVR

支持向量机SVMSVM的工作原理及分类支持向量机的原理线性可分的SVM非线性可分的支持向量机支持向量机分类SVC支持向量机回归SVRSVR原理SVR模型时间序列曲线预测SVM的工作原理及分类 支持向量机的原理 支持向量机(Support Vector Machine&#xff0c;SVM)是一种二类分类器&…

积极融入信创生态 | 思腾合力软件产品完成多个信创产品适配

从2019年我国提出发展信创产业&#xff0c;2020年迈入信创发展元年&#xff0c;到2022信创开始向行业深水区迈进&#xff0c;逐渐延伸到金融、电信等重点行业、核心业务中&#xff0c;开启了“行业信创元年”。一个真正的“大信创”时代已开启&#xff0c;一个数万亿规模的市场…

MybatisPlus---从入门到深化

目录 MyBatisPlus入门 MyBatisPlus介绍 ​编辑Spring集成MyBatisPlus SpringBoot集成MyBatisPlus MyBatisPlus_CRUD 添加 CRUD_相关注解 修改 删除 查询 条件构造器 全局配置 ActiveRecord_概念 ActiveRecord_增删改查 MyBatisPlus插件_插件概述 MyBatisPlus插件_…

超神之路 数据结构 3 —— Stack栈实现及应用

栈也是一种线性表结构&#xff0c;相较于数组&#xff0c;栈对应的操作是数组的子集&#xff0c;我们只要实现从一端添加元素&#xff0c;并从这个一端取出元素&#xff0c;这一端我们称呼它为栈顶&#xff0c;正是由于这种结构&#xff0c;它具有“后入先出”&#xff08;LIFO…

PTA题目 计算工资

某公司员工的工资计算方法如下&#xff1a;一周内工作时间不超过40小时&#xff0c;按正常工作时间计酬&#xff1b;超出40小时的工作时间部分&#xff0c;按正常工作时间报酬的1.5倍计酬。员工按进公司时间分为新职工和老职工&#xff0c;进公司不少于5年的员工为老职工&#…

基于jsp+mysql+ssm健身信息交流网站-计算机毕业设计

项目介绍 随着全民健身运动的兴起&#xff0c;越来越多的人走进了健身房&#xff0c;而传统的管理模式已不能适应现代健身机构的发展趋势&#xff0c;如何增强健身房会员卡的管理和完善客户服务&#xff0c;成了健身房发展的当务之急。健身信息管理系统的研究与开发&#xff0…