JavaSE-多线程(一)
多线程
基本概念
-
什么是程序
编写的代码就是程序
-
什么是进程
程序一旦运行起来,就要在内存中分配空间,产生一个进程。是系统资源分配的单位。任务管理器中可以查看进程。
-
什么是线程
进程中相对独立的执行单元,线程启动顺序具有不确定性。是CPU 执行调度的单位。
-
在java 中有多少个线程
main(主线程) 、GC (垃圾回收线程)、异常处理线程(抛出异常会影响main 线程的执行)。main() 称为主线程为系统的入口,用于执行整一个程序
-
什么是并发
在一个进程中,如果开辟了多个线程,形成的运行由CPU 安排调度,先后顺序不能人为干预。同一份资源操作时,会存在资源抢夺问题,需要加入并发控制。
- 进程和线程的区别
创建线程
继承Thread 类创建线程
创建线程与启动线程
类之间的关系:
类构造器:
继承Thread 类创建线程
继承线程类 Thread,有了多线程的能力就可以在同一进程中共享资源。
所以这个类称为线程类(实例化的对象就是线程对象)。
public class CreateThread_m_1 extends Thread {
// 执行的任务写到run 方法中
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("TestThread-------------:" + i);
}
}
}
执行任务 run()
重写run 方法添加任务。
主线程中启动其他线程 start()
public class Demo {
public static void main(String[] args) { // 主线程
for (int i = 0; i < 10; i++) {
System.out.println("main1----------:" + i);
}
// 创建其他线程
CreateThread_m_1 createThread_m_1 = new CreateThread_m_1();
// 执行到这一步该程序中存在3 个线程: 主线程 其他线程 垃圾回收线程
// createThread_m_1.run();// 直接执行任务,直接占用主线程
// 启动子线程
createThread_m_1.start();
for (int i = 0; i < 10; i++) {
System.out.println("main----------:" + i);
}
}
}
注意:不能直接run 线程任务,直接调用run 方法无法启动新线程,如果直接run 体现的是至上而下地执行任务,没有新的线程启动,start()方法才会启动新线程。线程启动后Java虚拟机调用此线程的run方法,同样还是遵照程序的自上而下执行。对于main 方法而言也遵照程序的自上而下执行。
输出结果:
main1----------:0
main1----------:1
main1----------:2
main1----------:3
main1----------:4
main1----------:5
main1----------:6
main1----------:7
main1----------:8
main1----------:9
main----------:0
TestThread-------------:0
TestThread-------------:1
main----------:1
TestThread-------------:2
main----------:2
main----------:3
TestThread-------------:3
main----------:4
TestThread-------------:4
main----------:5
TestThread-------------:5
main----------:6
TestThread-------------:6
main----------:7
TestThread-------------:7
main----------:8
TestThread-------------:8
main----------:9
TestThread-------------:9
线程名称
给线程设定名称
自定义线程:
public class CreateThread_m_1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 接名字
System.out.println(this.getName() + "-------------:" + i);
}
}
}
主线程:
public class TestThread {
public static void main(String[] args) { // 主线程
// 获取当前线程并设置
Thread.currentThread().setName("this is main thread");
CreateThread_m_1 createThread_m_1 = new CreateThread_m_1();
// 设置线程
createThread_m_1.setName("this is child thread");
createThread_m_1.start();
for (int i = 0; i < 10; i++) {
// 获取当前线程名称
System.out.println(Thread.currentThread().getName() + "----------:" + i);
}
}
}
或者直接调用有参构造器
子线程:
public class CreateThread_m_1 extends Thread {
public CreateThread_m_1(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + "-------------:" + i);
}
}
}
主线程:
public class TestThread {
public static void main(String[] args) {
Thread.currentThread().setName("this is main thread");
CreateThread_m_1 createThread_m_1 = new CreateThread_m_1("this is child thread");
// 启动子线程
createThread_m_1.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "----------:" + i);
}
}
}
如果使用Thread创建线程必须使用 static 修饰符修饰的变量参与资源共享
10 张火车票3 个窗口售卖,售完即止。
子线程:
public class TrainTestObj extends Thread {
// 资源共享
private static int ticketNum = 10;
public TrainTestObj(String name) {
super(name);
}
@Override
public void run() {
while (ticketNum > 0) {
System.out.println(this.getName() + "窗口卖了1张票,剩余" + (--ticketNum));
}
}
}
主线程:
public class TrainTest {
public static void main(String[] args) {
TrainTestObj trainTestObj = new TrainTestObj("3");
TrainTestObj trainTestObj2 = new TrainTestObj("2");
TrainTestObj trainTestObj3 = new TrainTestObj("1");
// 启动线程
trainTestObj.start();
trainTestObj2.start();
trainTestObj3.start();
}
}
输出结果发现了问题:这就和多线程的调度与资源共享有关。如何一步步执行线程任务?这就需要线程加锁。
CPU执行哪个线程的代码具有不确定性
2窗口卖了1张票,剩余8
1窗口卖了1张票,剩余7
1窗口卖了1张票,剩余5
3窗口卖了1张票,剩余9
3窗口卖了1张票,剩余3
3窗口卖了1张票,剩余2
3窗口卖了1张票,剩余1
3窗口卖了1张票,剩余0
1窗口卖了1张票,剩余4
2窗口卖了1张票,剩余6
实现Runnable接口创建线程
创建线程与启动线程
继承Runnable 类创建线程
注意这个类是一个接口类,无法重载父类构造器
子线程:
public class CreateThread_m_2 implements Runnable {
// 接口类无法定义构造器,所以无法通过this 获取线程名称
// 所以使用Thread.currentThread().getName() 获取
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i);
}
}
}
主线程:
需要与Thread 进行关联
public class TestThread2 {
public static void main(String[] args) {
Thread.currentThread().setName("this is main thread");
// 创建子线程
CreateThread_m_2 createThread_m_2 = new CreateThread_m_2();
// 关联线程
Thread thread = new Thread(createThread_m_2,"this is child thread");
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i);
}
}
}
购买火车票
class Thread_Runnable implements Runnable {
private int num = 10;
@Override
public void run() {
while (num > 0) {
System.out.println(Thread.currentThread().getName() + "买了一张票,剩余:" + (--num));
}
}
}
public class Demo {
public static void main(String[] args) {
Thread_Runnable thread_runnable = new Thread_Runnable();
new Thread(thread_runnable, "窗口1").start();
new Thread(thread_runnable, "窗口2").start();
new Thread(thread_runnable, "窗口3").start();
}
}
使用内部类:
public class Test {
private int num = 10;
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (num > 0) {
System.out.println(Thread.currentThread().getName() + "买了一张票,剩余:" + (--num));
}
}
}, "窗口1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
while (num > 0) {
System.out.println(Thread.currentThread().getName() + "买了一张票,剩余:" + (--num));
}
}
}, "窗口2");
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
while (num > 0) {
System.out.println(Thread.currentThread().getName() + "买了一张票,剩余:" + (--num));
}
}
}, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
输出结果:
窗口2买了一张票,剩余:9
窗口1买了一张票,剩余:8
窗口1买了一张票,剩余:6
窗口1买了一张票,剩余:5
窗口1买了一张票,剩余:4
窗口1买了一张票,剩余:3
窗口3买了一张票,剩余:2
窗口2买了一张票,剩余:7
窗口3买了一张票,剩余:0
窗口1买了一张票,剩余:1
使用lambda表达式
为什么可以使用lambda 表达式实现Runnable 接口类
原因就是Runnable 接口类是一个函数型接口
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// ()->{...} lambda 表达式
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.saleTick();
}
}, "窗口1").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.saleTick();
}
}, "窗口2").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.saleTick();
}
}, "窗口3").start();
}
}
class Ticket {
private int num = 50;
// synchronized 本质就是锁+ 队列
public /*synchronized*/ void saleTick() {
if (num > 0)
System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余:" + (--num));
}
}
实现Callable接口创建线程
第一种和第二种实现run 方法的缺点
-
没有返回值。
-
不能抛出异常。
所以经过上述分析的缺点,引入了方式三。
Runnable 和Callable 有什么不同?
主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。
实现Callable 接口类创建线程
JDK1.5 为了解决上述缺点而引入…
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(3000); // 异步测试,睡眠3 秒
return new Random().nextInt(10); // 生成一个10 以内的随机数
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Test test = new Test();
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(test);
// 中间转换器
Thread thread = new Thread(integerFutureTask);
thread.start();
// 获取线程返回结果,以及线程情况
System.out.println("线程是否执行完成" + integerFutureTask.isDone());
System.out.println(integerFutureTask.get()); //等待了3 秒再获取,说明get 方法是一个阻塞方法
System.out.println("线程是否执行完成" + integerFutureTask.isDone());
}
}
缺点:线程创建比较麻烦
购买火车票
class Thread_Callable implements Callable<Boolean> {
private int num = 10;
@Override
public Boolean call() throws Exception {
while (num > 0) System.out.println(Thread.currentThread().getName() + "买了一张票,剩余:" + (--num));
return true;
}
}
public class Demo {
public static void main(String[] args) {
Thread_Callable thread_callable = new Thread_Callable();
FutureTask<Boolean> booleanFutureTask = new FutureTask<Boolean>(thread_callable);
new Thread(booleanFutureTask, "窗口1").start();
new Thread(booleanFutureTask, "窗口2").start();
new Thread(booleanFutureTask, "窗口3").start();
}
}
输出结果:
窗口2买了一张票,剩余:9
窗口2买了一张票,剩余:8
窗口2买了一张票,剩余:7
窗口2买了一张票,剩余:6
窗口2买了一张票,剩余:5
窗口2买了一张票,剩余:4
窗口2买了一张票,剩余:3
窗口2买了一张票,剩余:2
窗口2买了一张票,剩余:1
窗口2买了一张票,剩余:0
多线程不安全
购买或称票不安全
//----------------------------------------------------------------------- Thread 版本
class Ticket extends Thread {
private static int tickNumber = 10;
public Ticket(String name) {
super(name);
}
@Override
public void run() {
// 人抢票直至抢完为止
while (tickNumber > 0) {
System.out.println(this.getName() + "抢了一张票,剩余:" + (--tickNumber));
}
}
}
public class Demo{
public static void main(String[] args) {
new Ticket("窗口1").start();
new Ticket("窗口2").start();
new Ticket("窗口3").start();
}
}
//----------------------------------------------------------------------- Runnable 版本
class Ticket implements Runnable {
private static int tickNumber = 10;
@Override
public void run() {
// 人抢票直至抢完为止
while (tickNumber > 0) {
System.out.println(Thread.currentThread().getName() + "抢了一张票,剩余:" + (--tickNumber));
}
}
}
public class Demo{
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
// new Thread(()->{},"窗口1").start();
}
}
//----------------------------------------------------------------------- Callable 版本
class Ticket implements Callable {
private static int tickNumber = 10;
@Override
public String call() throws Exception {
while (tickNumber > 0) {
System.out.println(Thread.currentThread().getName() + "抢了一张票,剩余:" + (--tickNumber));
}
return Thread.currentThread().getName();
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Ticket ticket = new Ticket();
FutureTask<String> stringFutureTask = new FutureTask<String>(ticket);
new Thread(stringFutureTask, "窗口1").start();
new Thread(stringFutureTask, "窗口2").start();
new Thread(stringFutureTask, "窗口3").start();
}
}
银行取钱业务不安全
class Account {
String name;
int money;
public Account(String name, int money) {
this.name = name;
this.money = money;
}
}
class TakeMoney extends Thread {
Account account; // 账户
int takeMoney; // 取多少钱
int money; // 手里多少钱
public TakeMoney(Account account, int takeMoney, String name) {
super(name);
this.account = account;
this.takeMoney = takeMoney;
}
@Override
public void run() {
if (account.money - takeMoney < 0) {
System.out.println(this.getName() + "取不了钱!");
return;
}
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - takeMoney;
money = money + takeMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里有:" + money + "钱");
}
}
public class Demo {
public static void main(String[] args) {
Account account = new Account("基金", 100);
new TakeMoney(account, 50, "你").start();
new TakeMoney(account, 50, "女朋友").start();
}
}
集合不安全
public class Demo extends Thread {
static List<String> list = new ArrayList<String>();
public Demo(String name) {
super(name);
}
@Override
public void run() {
list.add(this.getName());
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Demo("线程1").start();
}
System.out.println(list.size()); // 605
}
}
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
},"线程1").start();
}
System.out.println(list.size()); // 566
}
}
CopyOnWriteArrayList 线程安全。
对高并发的理解
并发编程
- 并发
CPU 一核,模拟出多条线程,天下武功唯快不破,快速交替。
单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了
- 并行
CPU 多核,多线程同时执行,线程池。
public class Test{
public static void main(String[] args){
System.out.println(Runtime.getRuntime().availableProcessors()); // 4
}
}
本质就是充分利用CPU 资源。
线程的调度与生命周期
什么是时间片、单核 / 多核CPU如何执行任务?
CPU分配给各个程序的时间,使各个程序从表面上看是同时进行的,而不会造成CPU资源浪费。
对于单核cpu来说,同一时间片只能执行一个线程。因为时间片时间特别短,感受到的就是“同时”执行多个线程实际上这个多线程只是一种假象。
对于多核cpu来说此时才是真正意义上的同一时间片执行多个线程。
什么是线程的上下文调度
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
线程生命周期
- 新生状态
-
使用new 关键字建立线程对象后,该线程对象就处于新生状态
-
处于新生状态的线程有自己的内存空间,通过start 进入就绪状态
- 就绪状态
-
处于就绪状态的线程具备了运行条件,但还没有分配到CPU 中,处于线程就绪队列,等待系统为其分配CPU
-
当系统选定一个执行的线程后,他就后从就绪状态进入运行状态,这个动作叫做CPU 的调度
- 运行状态
-
在运行状态的线程执行自己run 方法中的代码,直到等待某资源阻塞或完成任务而死亡
-
如果在给定的时间片内没有执行结束,就会被系统给换下来,回到等待执行状态
- 阻塞状态
-
处于运行状态的线程在某种情况下,如执行了sleep(睡眠)方法,或者等待IO设备等资源,将让CPU 暂停对自己的运行,进入阻塞状态
-
在阻塞状态的线程不能进入就绪状态。只有当引起阻塞的原因被消除时,如睡眠时间已到,或等待IO 设备空闲下来,线程才会重新进入就绪状态,重新到就绪队列中等待,被系统选中后从原来的位置开始继续运行
- 死亡状态
- 死亡状态是线程最后的一个阶段,死亡原因存在3 个。一、线程执行玩所有的任务。二、线程强制被终止(调用了stop方法)。三、线程抛出未捕获的异常
并发编程三大核心问题?
并发编程三大核心基础理论:原子性、可见性、有序性
原子性
什么是原子性
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意 为“不可被中断的一个或一系列操作”。
原子性,即一个操作或多个操作,要么全部执行并且在执行的过程中不被打断,要么全部不执行。(提供了互斥访问,在同一时刻只有一个线程进行访问)
原子性问题的产生的原因
当某个线程获得CPU的时间片之后就获取了CPU的执行权,就可以执行任务,当时间片耗尽之后还没执行完任务,就会失去CPU使用权。
进而本任务会暂时停止执行。多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
public class Demo {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100000);
for (int i = 0; i < 100000; i++) {
new Thread(() -> {
add();
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(num); // 99996
}
private static void add() {
num++;
}
}
产生这个问题的原因:
i++ 的执行实际上这个操作不是原子性的,因为 i++ 会被拆分成以下三个步骤执行(这样的步骤不是虚拟的,而是真实情况就是这么执行的)
-
读取i的值
-
计算+1 的结果
-
将+1 的结果赋值给i 变量
在多线程中会发生资源争抢问题,例如:
线程A 从主内存中读取i 的值并计算+1 结果,此时计算结果还未写回主内存中就被线程B 抢去了资源从主内存中读取i (0),线程B开始对 i 值进行+1操作; A线程将累加后的结果赋值给 count 结果为 1,B 线程将累加后的结果赋值给 count 结果为 1;A 线程将结果 count =1 刷回到主内存,B 线程将结果 count =1 刷回到主内存。这样就造成了最终i 不为10000 的原因。
解决方法:
- 加锁
// synchronized
public class Demo {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100000);
for (int i = 0; i < 100000; i++) {
new Thread(() -> {
add();
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(num); // 100000
}
private static synchronized void add() {
num++;
}
// private static void add() {
// synchronized (Demo.class) {
// num++;
// }
// }
}
// ReenterantLock()
public class Demo {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100000);
for (int i = 0; i < 100000; i++) {
new Thread(() -> {
add();
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(num); // 100000
}
private static void add() {
try {
lock.lock();
num++;
} finally {
lock.unlock();
}
}
}
- CAS 操作
public class Demo {
private static AtomicInteger aint = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100000);
for (int i = 0; i < 100000; i++) {
new Thread(() -> {
add();
countDownLatch.countDown();
}).start();
/*countDownLatch.countDown(); 注意此操作不能写在此处*/
}
countDownLatch.await();
System.out.println(aint); // 100000
}
private static void add() {
aint.getAndIncrement();
}
}
注意为什么不需要加volatile? 因为synchronized 保证可见性、原子性。由于i++ 不是原子操作(i=i+1)。由于依赖性不会进行指令重排,所以不需要volatile 修饰。
可见性
什么是可见性
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
可见性问题的产生的原因
JMM 中的约定:
-
所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
-
在多线程情况下,线程上锁之前将共享资源从主内存中加载到线程工作内存中。
-
在多线程情况下,线程解锁前必须更新主内存中的数据
-
线程对变量的所有的操作(读,写【写缓冲区】)都必须在工作内存中完成,而不能直接读写主内存中的变量。
public class Demo {
private static boolean bol = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bol = true;
}).start();
while (!bol) {
}
System.out.println("跳出循环了");
}
}
是否会输出”跳出循环了“?不会,缓存不能及时刷新到主内存就是导致可见性问题产生的根本原因。
解决方法:
- 加锁
public class Demo {
private static boolean bol = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bol = true;
}).start();
while (!bol) {
synchronized (Demo.class){}
}
System.out.println("跳出循环了");
}
}
- volatile
public class Demo {
private volatile static boolean bol = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bol = true;
}).start();
while (!bol) {
}
System.out.println("跳出循环了");
}
}
有序性
什么是可见性
有序性:程序执行的顺序按照代码的先后顺序执行。
有序性问题产生的原因
-
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。考虑依赖性
-
指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
-
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上 去可能是在乱序执行。
有序性的案例最常见的就是 DCL了(double check lock)就是单例模式中的双重检查锁功能。
public class Demo {
private int num;
private static Demo demo;
public Demo() {
num = 1;
}
public static Demo getInstance() {
if (demo == null) {
synchronized (Demo.class) {
if (demo == null) demo = new Demo();
// 1. 为Demo实例分配空间、初始化实例属性默认值
// 2. 调用构造器,初始化实例、实例属性
// 3. 返回地址给引用
// 多线程过程中会可能发生指令重排...还分配空间就返回地址给引用导致空指针异常。
}
}
return demo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
getInstance();
}).start();
}
}
}
解决方法:
- volatile
public class Demo {
private int num;
private volatile static Demo demo;
public Demo() {
num = 1;
}
public static Demo getInstance() {
if (demo == null) {
synchronized (Demo.class) {
if (demo == null) demo = new Demo();
}
}
return demo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
getInstance();
}).start();
}
}
}
**为什么加上volatile 就可以避免指令重排呢?**见下篇。