1、多线程的概述
多线程 可以被理解为 CPU 在多个 软件 之间进行高速的切换;
并行:同一时刻,多个指令在多个CPU上 同时 指行。
并发:同一时刻,多个指令在单个CPU上 交替 指行。
进程:一般指正在运行的 软件;独立行、动态性、并发性。
线程:进程中的单个顺序控制流,是一条执行语句。
2、多线程的实现方式
- 继承Thread类
step:
定义一个实体类继承 Thread 类
重写 run() 方法
创建实体类对象
调用 start() 方法启动线程
代码示例 多线程实现方式一
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i + "线程开启");
}
}
}
/**
* 线程测试
*/
public class ThreadDemo {
/*
线程的执行具有随机性
创建多条线程执行时并不按照线程创建顺序执行,而是随机
*/
public static void main(String[] args) {
// 创建线程对象
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
}
}
注意
run() 方法封装了被线程执行的代码,直接调用相当于普通的方法调用,没有开启线程;start() 表示启动线程由 JVM 调用线程的 run() 方法。
- 实现Runnable接口
step:
定义一个实体类实现 Runnable 接口
重写 run() 方法
创建自定义实体类对象
创建线程对象,把自定义实体类对象作为构造参数
调用 start() 方法启动线程
代码示例 多线程实现方式二
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i + "线程开启");
}
}
}
/**
* 线程测试
*/
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
- 利用Callable和Future接口实现
step:
定义一个实体类实现 Callable 接口
重写 call() 方法
创建自定义实体类对象
创建Future的实现类 FutureTask 对象,把自定义实体类对象作为构造参数
创建Thread类对象,把FutureTask对象作为构造参数
调用 start() 方法启动线程
代码示例 多线程实现方式三
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(i + "线程开启");
}
return "FutureTask对象可以获取线程执行完毕之后的结果";
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 线程测试
*/
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// call()方法封装线程执行的代码
MyCallable myCallable = new MyCallable();
// 可以获取线程执行完毕之后的结果,也可以作为参数传递给Thread对象
FutureTask<String> task = new FutureTask<>(myCallable);
Thread thread = new Thread(task);
thread.start();
String result = task.get();
System.out.println(result);
}
}
3、Thread的成员
方法/变量 | 解释 |
---|---|
name | Thread的属性,有get()、set()方法可以使用 |
public static Thread currentThread() | 返回当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
public final void setPriority(int newPriority) | 设置线程优先级(0-10, 5为默认) |
public final int getPriority() | 获取线程优先级 |
public final void setDaemon(boolean on) | 设置守护线程,随着被守护线程的结束慢慢结束 |
线程调度
- 分时调度:所有线程 轮流 使用 CPU 的使用权,平均分配每个线程占用 CUP 的时间。
- 抢占式调度:优先(并不绝对)让优先级高的线程使用 CPU,若优先级相同,则随机使用 CPU 的执行权。
4、线程安全
代码分析线程安全 多线程实现三个三个窗口卖90张票
public class Ticket implements Runnable{
// 票数
private int ticket = 90;
@Override
public void run() {
while (true) {
if (ticket <= 0) {
break;
} else {
try {
// 线程休眠 100 毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "窗口在卖票还剩下" + ticket + "张票");
}
}
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 线程测试
*/
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 90张票
Ticket ticket = new Ticket();
// 3个售票窗口
Thread thread1 = new Thread(ticket,"窗口一");
Thread thread2 = new Thread(ticket,"窗口二");
Thread thread3 = new Thread(ticket,"窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果
问题
执行结果出现,窗口剩余重复票数,出现负票数的安全问题。出现这样的原因是 线程休眠时 在run()方法的循环中出现线程抢占 共享数据 ticket而引发的。
解决方案:同步代码块、同步方法、Lock(jdk1.5之后)
- 同步代码块(锁对象唯一 )
格式:
synchronized(任意对象) {
多条语句操作共享数据
}
同步代码块锁住多条语句操作的共享数据,默认情况下锁是打开的,当一个线程进去执行代码了,锁就会关闭;当线程执行完出来,锁就会自动打开。
好处:解决了线程安全问题
弊端:线程逐个进入同步代码块,效率低
多线程实现三个三个窗口卖90张票 同步代码块解决线程安全
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket == 0) {
break;
} else {
try {
// 线程休眠 100 毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "窗口在卖票还剩下" + ticket + "张票");
}
}
}
}
- 同步方法(锁对象this)
格式:
修饰符 synchronized 返回值类型 方法名(参数) { }
同步代码块和同步方法的区别:
同步代码块:可以锁住部分代码,不可以指定锁对象
同步方法:可以锁住所有代码,可以指定锁对象
多线程实现三个三个窗口卖90张票 同步代码块解决线程安全
@Override
public void run() {
while (true) {
boolean result = synchronizedMethod();
if (result) {
break;
}
}
}
private synchronized boolean synchronizedMethod() {
if (ticket == 0) {
return true;
} else {
try {
// 线程休眠 100 毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "窗口在卖票还剩下" + ticket + "张票");
return false;
}
}
- Lock
Lock是接口不能直接实例化,可以通过实现类 ReentrantLock 来实例化ReentrantLock的构造方法。
多线程实现三个三个窗口卖90张票 同步代码块解决线程安全
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private static int ticket = 90;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
if ("窗口三".equals(Thread.currentThread().getName())) {
try {
lock.lock();
if (ticket == 0) {
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "窗口在卖票还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
}
- 死锁
线程死锁是由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。
- 等待和唤醒
方法名 | 解释 |
---|---|
void wait() | 使线程处于等待状态, |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
5、阻塞队列
ArrayBlockingQueue:底层是数组,有界。
LinkedBlockingQueue:底层是链表,无界(最大为 int 的最大值)
put(anObject):将参数放入队列,如果放不进去会阻塞。
take():取出第一个数据,取不到会阻塞。
阻塞队列的练习
import java.util.concurrent.ArrayBlockingQueue;
public class Consumer extends Thread {
private ArrayBlockingQueue<String> list;
public Consumer(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
String take = list.take();
System.out.println("消费者从阻塞队列获取产品" + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Producer extends Thread {
private ArrayBlockingQueue<String> list;
public Producer(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
list.put("产品");
System.out.println("生产者放入一个产品");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Demo {
public static void main(String[] args) {
// 创建一个阻塞队列,容量为1
ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(1);
Consumer consumer = new Consumer(list);
Producer producer = new Producer(list);
consumer.start();
producer.start();
}
}
6、线程状态
NEW:新建状态,创建线程对象时的状态。
RUNNABLE:就绪状态,线程对象调用start()方法之后的状态。
BLOCKED:阻塞状态,线程对象无法获得锁对象,
WAITING:等待状态,调用 wait() 方法时所处的状态。
TIMED_WAITING:计时等待,调用 sleep() 方法时所处的状态。
TERMINATED:结束状态,代码全部执行完毕之后。
7、线程池
不使用线程池的弊端,频繁的创建和销毁线程,浪费资源;线程池 的出现解决了这个弊端。
线程池的原理
- 首先创建一个空的线程池;
- 当需要使用线程时,创建一个线程对象 ;
- 使用结束之后放回线程池中;
- 再次使用时直接从线程池取出;
- 当线程池中的线程对象正在被使用时,若还需要线程时再次创建。
Executors 的静态方法创建线程池对象,一般使用 newCacheThreadPool 创建一个默认的线程池对象,newFixedThreadPool(int nThread) 创建一个线程池,由参数指定最大线程对象容量。这两个方法的底层都是通过创建 ThreadPoolExecutor 完成的。
submit() 线程池会自动创建线程对象,任务执行完毕之后再归还给线程池。
shutdown() 关闭线程池。
ThreadPoolExecutor的参数
- 核心线程数量(随着线程池的存在而存在)
- 最大线程数量(核心线程数量 + 临时线程数量)
- 空闲时间(值,决定临时线程的存在)
- 空闲时间(单位)
- 阻塞队列(当任务大于最大线程数量时,多余的任务将会进入阻塞队列)
- 线程创建方式(一般采用默认方式)
- 拒绝策略(当任务大于最大线程与阻塞队列总数量时,多余的任务将会被拒绝策略处理)
线程池参数的练习
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
3,
6,
2,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 10; i++) {
poolExecutor.submit(new MyRunnable());
}
poolExecutor.shutdown();
}
}
拒绝策略
方法名 | 解释 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略,丢弃任务并抛出异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常, |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃阻塞队列中等待最久的任务,加入当前任务 |
ThreadPoolExecutor.CallerRunsPolicy | 调用任务的run()方法绕过线程池直接执行 |
8、Volatile
我们通过一个小案例来介绍 volatile
甲乙两个人共同存了1000元,当甲动用了1000元之后,乙没有及时获取最近数据,任然认为还有1000元。这在多线程中就相当于甲乙两个线程不能及时获取刷新后的共享数据。
解释出现此现象的原因
乙线程获取共享数据,将共享数据在线程栈中做临时存储(变量副本),之后使用数据直接用变量副本的数据。
甲线程获取共享数据,将共享数据在线程栈中做临时存储(变量副本)
之后甲线程将变量副本的数据改为900,再将变量副本的值赋值给共享数据,但此时乙线程并未从共享数据中得到更新后的数据。
Volatile 关键字可以强制线程使用变量副本的数据时,去共享数据中查看是否有最近的数据值。
使用方法 volatile 作共享数据修饰符
public static volatile int money = 1000;
也可以使用 synchronized 同步代码块解决,synchronized 可以使线程获得锁之后,清空变量副本,拷贝最近的共享数据到变量副本中(同样具有强制线程查看更新共享数据到变量副本的作用)。
9、原子性
原子性一般指多个操作是一个不可分割的整体,要么同时成功,要么同时失败;例如:甲要给乙转账1000,不能出现甲转账失败,但乙又接收成功的现象,因为这样就会凭空多出1000,不合理。
volatile 不可以解决原子性问题,因为只是强制其他线程去共享数据中查看最新数据,并未强调最新数据一定会在其他线程抢到CPU执行权之前将最新数据从变量副本赋值到共享数据。虽然 synchronized 可以解决此问题,但太过于繁琐,我们一般采用 AtomicInteger 类去解决此问题。
AtomicInteger 类
方法名 | 解释 |
---|---|
public AtomicInteger() | 初始化一个默认值为0的原子型Integer |
public AtomicInteger(int initialValue) | 初始化一个指定值的原子型Integer |
int get() | 获取值 |
int getAndIncrement() | 以原子方式将当前值加1,注意,这里的返回值是自增前的值 |
int incrementAndGet() | 以原子方式将当前值加1,注意,这里的返回值是自增后的值 |
int addAndGet(int data) | 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果 |
int getAndSet(int value) | 以原子方式设置为newValue的值,并返回旧值 |
方法练习
import java.util.concurrent.atomic.AtomicInteger;
public class Demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger); // 10
int a = atomicInteger.get();
System.out.println(a); // 10
int b = atomicInteger.getAndIncrement();
System.out.println(b); // 10 返回旧值
int c = atomicInteger.incrementAndGet();
System.out.println(c); //12 返回新值
int d = atomicInteger.addAndGet(20);
System.out.println(d); // 32 返回相加后的值
int e = atomicInteger.getAndSet(10); // 设置新值
System.out.println(e); // 32 返回旧值
System.out.println(atomicInteger); // 10
}
}
代码练习 解决原子性
import java.util.concurrent.atomic.AtomicInteger;
public class MyRunnable implements Runnable {
AtomicInteger ac = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int count = ac.incrementAndGet();
System.out.println("已经转账" + count + "次");
}
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
for (int i = 0; i < 10; i++) {
new Thread(mr).start();
}
}
}
AtomicInteger 原理
自旋锁 + CAS算法
CAS算法:有三个操作(内存值V,旧的预期值A,要修改的值B)当旧的预期值A == 内存值 此时修改成功,将 V 改为 B;当旧的预期值A != 内存值 此时修改失败,不做任何操作并重新获取现在的最新值(这个重新获取的动作就是自旋)
A 线程从堆内存中获取 内存值 V,将内存值赋值给 A 线程栈中的变量副本,之后操作的是变量副本中的值,也叫做旧值 A。
当旧的预期值 A 经过修改后,变量副本的值发生了修改,出现了要修改的值 B;此时要进行修改操作就算将 B 赋值给 V。
在进行值修改前,B 线程也经历了与 A 线程同样的操作,两个线程都要对 V 进行修改。
此时 A 线程优先抢夺到 CPU 的执行权,将旧值 A 与 内存值 V 进行比较,结果相等,则将要修改的值 B 赋值 给内存值 V,V = 101。此时 B 线程获取 CPU 执行权,要进行修改时,发现旧的预期值 A(100)已经不等于内存值 V(101)了,则不作任何操作。
此时 B 线程要重新从都内存中获取最新的内存值 V(101)这个重新获取的动作就是自旋,然后在进行变量副本赋值,值修改等操作。比较时发现旧值 A(101)等于内存值V(101),这时,可以将要修改的值 B(102)赋值个内存值 V。
悲观锁:从最坏的角度出发,认为每次获取数据时,都有数据修改的可能,所以每次操作共享数据之前都会上锁。例如 synchronized。
乐观锁:假设每次获取数据都不会有数据修改,不上锁,只是在修改共享数据时,会检查一下,共享数据有没有被修改过。例如 cas。
10、Map集合的线程安全
HashMap是线程不安全的,多线程环境下会出现线程安全问题
Hashtable是线程安全的,但是会将整张表锁起来,效率低
ConcurrentHashMap也是线程安全的,效率高。
ConcurrentHashMap的原理
在1.7之前,ConcurrentHashMap 的底层是一个长度为 16 的数组。0 索引的值是一个默认长度为 2 的小数组,其他索引处的值为 null。当添加元素时,首先根据键的哈希值,计算元素在长度为 16 的数组中的索引位置,如果索引处的值为 null,则创建长度为 2 的小数组扩容因子为 0.75 。然后根据键的哈希值进行第二次计算,计算出元素在小数组中的索引位置。如果索引处值为空,则直接添加;如果索引处值不为空则调用 equals() 方法进行属性值比较,相同则不存,不相同则挂在旧元素上面形成链表。线程安全的原理在于添加元素时,小数组会加上锁,当两个或多个元素经过第一次哈希值计算要加入到相同的索引位置时,会触发锁机制,进行排队。
在1.8之后,底层结构是(数组 + 链表 + 红黑树),线程安全采用 CAS + synchronized 的形式保证。添加元素是,先根据键的哈希值计算出要存入的数组的索引位置,如果值为null,则利用 cas 进行元素添加;如果不为 null,则利用 volatile 关键字获取当前位置最新的节点地址,挂在下面形成链表。链表长度大于等于 8 时自动变成红黑树;synchronized 锁的对象是链表和红黑树。