多线程
进程:资源分配的基本单位,在程序执行时创建。
线程:资源分配的最小单位,是进程的一个执行流,一个进程由多个线程组成。
并发和并行:并发是指多个事件在一个时间段发生(交叉进行),并行是指多个事件在同一时间发生(同时进行)。
线程的调度方式
分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java使用的方式。
创建线程的两种方式
继承Thread类:用一个类继承Thread类然后重写run方法,用方法来完成线程任务。启动方式是在运行线程中可以实例化一个线程实例,然后调用start方法就可以开启创建的线程。(一个线程对象只能启动一次。)缺点:因为Java是单一继承制,无法满足线程类需要继承别的类来完成线程任务的情况。
实现Runable接口:用一个类实现Runable接口然后重写run方法,用方法完成线程任务。也是实例化一个线程类对象后,再将线程类对象作为参数示例化一个Thread类对象,然后调用start方法来开启线程。不过这种方法因为是使用的接口,所以并不会有单继承的烦恼。
//第一种
public class RunnableImpl extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
//第二种
public class RunnableImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
线程的随机性问题:执行主方法的线程我们称为主线程,当主线程完成一个线程对象的执行,并让主线程后子线程都进行控制台打印时我们会发现有可能出现随机打印的情况,这种情况产生的原因就是我们之前说的线程的调度方式,当我们两个线程都运行时相当于cpu有两条执行的通道,而两个线程都会抢夺cpu的执行权,所有cpu在随机访问两个线程之间切换就会出现随机打印的问题。
常用的关于线程信息的方法
//获取当前线程的名称
Thread.currentThread().getName()
//获取当前线程的优先级
Thread.currentThread().getPriority()
//设置线程的名称
//第一种
MyThread mt = new MyThread();
mt.setName("小强"); mt.start();
//第二种
new MyThread("旺财").start();
并发时的线程安全问题
出现的原因:多个线程操作同一个资源时,导致操作结果错误。
哪些问题:
1.可见性 :当一个公共资源被一个线程修改后,别的线程要能够看见它的修改。
分析:拿公共静态资源变量举例,如果主线程在拿到公共资源副本后对齐进行while循环判断其实没有对副本的操作并且高效的调用副本,所以这个副本就不会刷新,而在这个时候子线程对这个公共资源进行了操作,也获取了副本并且操作完成后返回给了静态资源区的公共资源。及时此时的公共资源满足了主线程跳出while循环的条件也不会跳出循环,因为主线程的副本没有刷新。
2.有序性 :程序的执行顺序按照代码的顺序执行。
分析 :有时候编译器在编译代码时可能会对一部分代码进行重排,重排的代码一般都是在单线程情况下不影响结果的,例如 a=1; b=2; 这种,但是在多线程环境下可能会影响别的线程的执行结果,例如a,b两个全局变量在别的线程中也在时候,此线程当一个值改变后丢失了CPU的控制权,开始执行另一个线程,如果没有重排则另一个线程操作的是改变后的a值,但是重排后操作的是改变后的b值。
3.原子性 :对资源操作时要么全部执行完成并且执行过程不会被任何元素打断,要么就都不执行。
分析:首先我们知道线程对公共资源的操作是三个部分。首先获取公共资源的副本,然后对副本进行操作,然后把结果返回给公共资源。在这个操作过程中可能随时丢掉cpu的使用权,所以可能出现一个线程拿到了副本,操作了数据然后还没返回丢失了cpu使用权,这个时候另一个线程拿到cpu使用权后开始拿副本操作数据,返回给公共资源,完成后第一个线程再次得到cpu使用权直接把数据写会,这样就会出现丢失了第二个线程对数据的操作情况,从而导致结果错误。
线程安全问题的解决方法
1.volatile关键字
此关键字作用在被多个线程操作的公共资源上其作用就是当公共资源被改变后使它的所有副本失效。并且修饰的变量在编译时不会发生顺序改变。所以可以解决可见性和有序性问题。但是还是同一情况,假如我们的回写过程是
int i;
tem = i + 1;
i = tem;
此时因为i被改变所以第一个线程i的值无效了,但它拿到cpu时会执行 i= tem 还是会导致结果错误。所以无法解决原子性问题。
2.原子类(CAS乐观锁)
提供的原子类
1). java.util.concurrent.atomic.AtomicInteger:对int变量进行原子操作的类。
2). java.util.concurrent.atomic.AtomicLong:对long变量进行原子操作的类。
3). java.util.concurrent.atomic.AtomicBoolean:对boolean变量进行原子操作的类
可以使用这些原子类来操作数据保证解决原子性问题,它是通过CAS乐观锁的方式来解决的
CAS乐观锁:它叫乐观锁是因为总是认为自己在访问数据操作是不会有别的线程也访问数据,所以这个加锁方式是允许自己操作的时候别的线程也操作的,但是当这个线程要进行数据返回前会进行一次判断,它会新获得一个副本并和自己的操作副本进行比较,如果不一样则说明在操作数据期间有别的线程也对数据进行了操作,这时它会用新的副本再操作一次数据再进行比较,如果一样则说明没有别的线程在这期间操作数据所以之间把数据写回。
CAS乐观锁的ABA问题:假如我们第一次取值副本为A然后有别线程在判断之前将公共资源修改为了B然后又修改回了A,那么开始的线程在判断时依旧会认为没有别的线程修改过公共资源而直接写回。(有人可能觉的反正都是A,应该没区别,可是如果是个栈对象,最开始的A已经出栈了这时栈口的A不是最开始的A了就会有问题。)所以解决办法是给每次对资源的修改增加一个版本号,判断时不再比较值,而是比对版本号。
代码级的线程安全问题
举例说明:银行存取款问题
public class Account {
//公共资源同一张卡 有卡号 余额
private String name ;
private double money;
public Account(String name, double money) {
this.name = name;
this.money = money;
}
getter...setter....
}
//取钱的线程类
public class Take implements Runnable{
private Account account;
private int takeMoney;
public Take(Account account, int getMoney) {
this.account = account;
this.takeMoney = getMoney;
}
@Override
public void run() {
if (takeMoney <=account.getMoney()){
System.out.println(Thread.currentThread().getName()+"取钱"+ takeMoney);
try {
Thread.sleep(1000);
System.out.println("剩余"+(account.getMoney()- takeMoney));
account.setMoney(account.getMoney()- takeMoney);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试类
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Account account = new Account("卡1",800);
new Thread(new Take(account,800)).start();
new Thread(new Take(account,800)).start();
}
}
//控制台打印
Thread-0取钱800
Thread-1取钱800
剩余0.0
剩余0.0
我们在这个例子中可以看见结果是错误的,预想的结果是第一次取钱取完后没有钱了,第二次取完钱后是-800块才对,但是因为取的是副本所以流程就变成了第一个线程执行,获得公共类的副本,然后在还没有操作回写改变公共值时第二个线程执行,获得公共副本,此时两个线程取的值都为800,所以其中一个线程执行完成回写公共资源为0,另一个并不会再重新获取公共资源副本而是把自己操作的结果0直接覆盖上个线程的值来改变公共资源。
解决办法
1.同步代码块
synchronized{
}
同步代码块中写需要保持原子性的部分在这个例子中
synchronized(account) {
if (takeMoney <=account.getMoney()){
System.out.println(Thread.currentThread().getName()+"取钱"+ takeMoney)
System.out.println("剩余" + (account.getMoney() - takeMoney));
account.setMoney(account.getMoney() - takeMoney);
}
}
原理:拿到CPU执行权的线程在代码执行到同步代码块时会判断同步代码块上是否有锁对象,如果有就获得锁对象并进入同步代码块执行,此时就算丢失CPU控制权别的线程执行到这也拿不到锁对象,直到有锁对象的线程执行完同步代码块内容然后释放锁对象别的线程才能获取锁对象进入执行。
2.同步方法
就是在方法上添加一个synchronized()修饰来让这个方法变成同步方法,基本等同于同步代码块。
3.Lock锁
简单的例子 具体的使用和synchronized 的区别可以去https://www.cnblogs.com/handsomeye/p/5999362.html
学习。
//1.在成员位置创建Lock接口的实现类对象
private ReentrantLock rl = new ReentrantLock();
//2.在可能出现线程安全问题的代码前,使用lock方法获取锁对象
rl.lock();
//3.在线程安全问题的代码后释放锁对象
rl.unlock();
常用的并发包
List集合的CopyOnWriteArrayLis。
set集合的CopyOnWriteArraySet。
Map集合的ConCurrentHashMap。
这几个并发包都是线程安全的,可以在多线程环境下保证线程安全。
多线程协作
1.CountDownLatch计数器
CountDownLatch使一个或多个现在在别的线程结束后完成操作。
构造方法
public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象
常用方法
public void await() throws InterruptedException// 让当前线程等待
public void countDown() // 计数器进行减1
实现
//需要等待的线程
public class MyThreadAC extends Thread {
private CountDownLatch countDownLatch;
public MyThreadAC(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("A");
try {
countDownLatch.await();//等待计数器归0执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
}
//执行后计数器减一的线程
public class MyThreadB extends Thread {
private CountDownLatch countDownLatch;
public MyThreadB(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("B");
countDownLatch.countDown();//让计数器的值-1
}
}
//运行的主线程
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(1);//创建1个计数器
new MyThreadAC(cdl).start();
Thread.sleep(1000);
new MyThreadB(cdl).start();
}
2.CyclicBarrier
他可以让多个线程执行到一个步骤后进行等待,直到所有线程都进入等待状态再执行莫一个线程。
构造方法
public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景
常用方法
public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
实现
//等待的线程
public class PersonThread extends Thread {
private CyclicBarrier cb;
public PersonThread(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
try {
Thread.sleep((int) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName()+"...来到了会场!");
cb.await();//cb内部会将计数器 - 1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
//等待后执行的线程
public class MeetingThread extends Thread{
@Override
public void run() {
System.out.println("线程都执行了,该我执行了!");
}
}
//主线程
public class Demo {
public static void main(String[] args) {
//等待5个线程执行完毕,再执行MeetingThread
CyclicBarrier cb = new CyclicBarrier(5,new MeetingThread());
PersonThread p1 = new PersonThread(cb);
PersonThread p2 = new PersonThread(cb);
PersonThread p3 = new PersonThread(cb);
PersonThread p4 = new PersonThread(cb);
PersonThread p5 = new PersonThread(cb);
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
}
}
3.Semaphore
控制线程的数量
构造方法
public Semaphore(int permits) permits 表示许可线程的数量
public Semaphore(int permits, boolean fair) fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
常用方法
public void acquire() 表示获取许可 lock
public void release() 表示释放许可 unlock
实现
//创建一个可以同时供两个人参观的教室类
package Demo14Semaphore;
import java.util.concurrent.Semaphore;
public class ClassRoom {
private Semaphore semaphore = new Semaphore(2);
public void info() throws InterruptedException {
semaphore.acquire();//开始同步
System.out.println(Thread.currentThread().getName()+"..来到了教室!");
Thread.sleep(2000);//参观2秒
System.out.println(Thread.currentThread().getName()+"..离开了教室!");
semaphore.release();//结束同步
}
}
//创建线程参观教室
public class StudentThread extends Thread{
private ClassRoom classRoom;
public StudentThread(ClassRoom classRoom) {
this.classRoom = classRoom;
}
@Override
public void run() {
try {
classRoom.info();//学生进入教室
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//主线程模拟多个游客(线程)参观
public class Demo {
public static void main(String[] args) {
//创建一个教室
ClassRoom classRoom = new ClassRoom();
//循环创建5名学员
for (int i = 0; i < 5; i++) {
new StudentThread(classRoom).start();
}
}
}
4.Exchanger
实现线程之间的数据交互
构造方法
public Exchanger()
常用方法
public V exchange(V x) 参数传递给对方的数据,返回值接收对方返回的数据
实现
//线程A
public class ThreadA extends Thread {
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程A开始执行");
System.out.println("线程A给线程B100元,并从线程B得到一个电影票");
String result = null;
try {
result = exchanger.exchange("100元");
System.out.println("线程A得到的东西:"+result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程B
public class ThreadB extends Thread {
private Exchanger<String> exchanger;
public ThreadB(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程B开始执行");
System.out.println("线程B给线程A一张电影票,并从线程A得到100元钱");
String result = null;
try {
result = exchanger.exchange("电影票");
System.out.println("线程B得到的东西:"+result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试类
public class Demo {
public static void main(String[] args) {
//创建Exchanger对象
Exchanger<String> exchanger = new Exchanger<>();
new ThreadA(exchanger).start();
new ThreadB(exchanger).start();
}
}
线程A开始执行
线程A给线程B100元,并从线程B得到一个电影票
线程B开始执行
线程B给线程A一张电影票,并从线程A得到100元钱
线程A得到的东西:电影票
线程B得到的东西:100元
//还可以设置等待的时间
try {
result = exchanger.exchange("100元",5, TimeUnit.SECONDS);
System.out.println("线程A得到的东西:"+result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("5秒钟没等到线程B的值,线程A结束!");
}
线程池
线程池简单说就是放线程的容器,在接受到线程任务后分配线程完成任务,在任务完成后将线程归还。线程带来的好处
1、降低了资源消耗,减少了任务执行需要创建和销毁线程消耗的资源
2、提升了响应速度,没有了创建线程的过程,任务直接执行。
3、提高了线程的可管理性。线程是需要限制创建的,要不然回消耗系统资源也会降低系统的稳定性。
线程池可以执行Runnable线程和Callable线程
区别也就是Runnable线程只能执行run 方法,而Callable可以有返回值返回值类型在创建时确定,并用future对象接受,调用起get方法获得。
例子
//求输入1+到输入n的和
public class Demo03ThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.使用Scanner获取一个整数
System.out.println("请输入一个整数:");
int n = new Scanner(System.in).nextInt();
//2.使用Executors线程池工厂类中的静态方法newFixedThreadPool创建一个指定线程数量的线程池ExecutorService
ExecutorService es = Executors.newFixedThreadPool(3);
//3.创建Callable接口的实现类对象,重写call方法,设置线程任务(计算1到n的和,并返回)
//4.使用线程池ExecutorService中的方法submit,传递线程任务(Callable),submit方法会在线程池中获取一个线程,执行线程任务
Future<Integer> future = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//计算1到n之间的和
//定义一个变量,初始值为0,记录累加求和
int sum = 0;
//使用for循环获取1到n之间的数字
for (int i = 1; i <= n; i++) {
//累加求和
sum += i;
}
//把和返回
return sum;
}
});
System.out.println("计算1到"+n+"之间的和为:"+future.get());
}
}
创建线程
Executors(jdk1.5并发包)提供四种线程池(具体可以看https://blog.csdn.net/qq_38408785/article/details/91383959)
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
再有就是通过ThreadPoolExecutor,它是Execurors 的底层实现。
具体的方式如下。
创建线程的参数
线程池的执行任务的流程
1.首先任务接受后判断现在的线程数是否达到了核心线程数,如果没达到就创建线程来执行任务,如果达到了就进入队列等待。
2.在进入队列时如果队列已经满了,那么就判断线程数是否达到了最大线程数,如果没有达到就创建线程执行任务,如果达到了就执行拒绝策略。
拒绝策略
1.当拒绝任务时抛出异常处理(默认)
2.当任务满时删除队列中最前面的任务再重新请求
3.直接抛弃请求不抛出异常
4.由请求线程自己来进行任务处理
死锁
就是在两个线程互相等待对方的锁对象的情况。
public class Die1 {
public static String lock = "1";
public static String lock1 = "2";
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1){
System.out.println("子线程进入了第一层的锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock){
System.out.println("子线程进入了第二层的锁");
}
}
}
}).start();
synchronized (lock){
System.out.println("主线程进入了第一层的锁");
Thread.sleep(2000);
synchronized (lock1){
System.out.println("主线程进入了第二层的锁");
}
}
}
}
主线程进入了第一层的锁
子线程进入了第一层的锁
线程状态
wait
无限等待,但会释放监视器(锁)为了让监视器通过别的线程调用notify方法唤醒
sleep
有限等待,不会释放监视器(锁)
Object类中的方法:
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify()
唤醒在此对象监视器(锁对象)上等待的单个线程。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
注意:
一般都在同步代码块中调用wait或者notify方法
一般都是使用锁对象调用wait和notify方法
锁对象-->wait()-->Thread-0线程-->等待
锁对象-->notify()-->Thread-1线程-->唤醒