多线程
讲多线程前先来了解下面几个概念;
进程、线程、多任务、程序
进程:其实说白了就是程序,只不过进程是动态的一个概念,而一个进程里往往有着>=1个的线程,比如,你在看直播的时候,技能看弹幕,又能听声音,这种肯定不是一个线程能处理的,不过一般多线程,是对应多个CPU的。
程序:是指令和数据的有序集合是静态的一个概念。
多任务:就是说一个人要执行很多的任务,比如你在吃饭的时候玩手机。
线程:就是独立执行的路径
1.核心概念
- ◆线程就是独立的执行路径;
- ◆在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- ◆main() 称之为主线程,为系统的入口,用于执行整个程序;
- ◆在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
- ◆对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- ◆线程会带来额外的开销,如cpu调度时间,并发控制开销。
- ◆每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2.线程的状态图:
线程有以下五种状态,具体如图所示,图里介绍的很明白。
具体想看的话可以通过代码输出线程的状态查看,如下所示:
package 线程的三种创建方式;
public class ThreadState {
public static void main(String[] args) {
Thread t=new Thread(()->{
for (int i=0;i<5;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("------------");
});
//创建的时候的状态
Thread.State state=t.getState();
System.out.println(state);
//启动后的状态
t.start();
state=t.getState();
System.out.println(state);
while(state !=Thread.State.TERMINATED){
//只要线程不终止,就一直输出他运行的状态
try {
Thread.sleep(100);
state=t.getState();
System.out.println(state);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3. 线程的一些方法
方法 | 说明 |
---|---|
join() | 就是插队执行,等加入的线程执行完了,再去执行其他的 |
yield() | 通俗说就是让出cpu和其他线程一起再次竞争资源 |
getPriority() | 获取线程优先级 1~10 默认值为5 优先级越高被优先调用的频率越高 |
setDaemon() | setDaemon(true) 设置当前线程为守护线程 默认false就是用户线程,当用户线程结束了守护线程也就结束了一般 |
interrupt() | 中断线程 由运行状态到死亡状态 |
getName()和setName() | getName() 获取此线程的名字 setName() 设置此线程的名字 |
补充:一般停止线程不用stop,通过外部标志位来控制结束。
4. 线程的3中创建方式Thread、Runnable、Callable
补充下:其实Callable底层也是通过前面两种的实现的
4.1 继承Thread类
示例代码:
package 线程的三种创建方式;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class Test01 extends Thread{
private String url;
private String name;
public Test01(String url,String name){
this.url=url;
this.name=name;
}
public static void main(String[] args) {
Test01 test01=new Test01("https://image.baidu.com/search/detail","mao.jpg");
test01.start();
}
@Override
public void run() {
DownLoader downLoader=new DownLoader();
downLoader.down(url,name);
System.out.println("下载了");
}
}
class DownLoader{
public void down(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("下载异常出现问题");
}
}
}
4.2 实现Runnable接口
示例代码如下:
package 线程的三种创建方式;
//
public class Test02 implements Runnable {
private int tickenum=10;
public void run() {
while (true){
if (tickenum<0){
break;
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+tickenum--+"票");
}
}
public static void main(String[] args) {
Test02 test02 = new Test02();
new Thread(test02,"小明").start();
new Thread(test02,"黄牛").start();
}
}
4.3实现Callable接口
示例代码如下:
package 线程的三种创建方式;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
public class Test03 implements Callable<Boolean>{
private String url;
private String name;
public Test03(String url,String name){
this.url=url;
this.name=name;
}
public static void main(String[] args) throws ExecutionException,InterruptedException {
Test03 t=new Test03("https://image.baidu.com/search/detail","mao.jpg");
//创建执行服务
ExecutorService service= Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> result1=service.submit(t);
//获取结果
boolean falg1=result1.get();
//关闭服务
service.shutdown();
}
public Boolean call() {
WebDownLoader downLoader=new WebDownLoader();
downLoader.down(url,name);
System.out.println("下载了");
return true;
}
}
class WebDownLoader{
public void down(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("下载异常出现问题");
}
}
}
5. 线程同步问题
说明一下什么叫线程同步?
简单的说就是一种等待机制,就是说要用当前对象的线程需要进入排好队一个个个使用。但是光排好队是不够的,打个比方就像上厕所一样,人都排好队进厕所但是第一个进去之后没关门然后第二个人又来了,那这就不行了。所以线程同步不仅需要排好队还需要一把锁。所以为了保证安全性,必须要加入锁机制,也就是synchronized。
但是加锁了也会存在一些问题?
-
在多线程的情况下,加锁,放锁都会引起较多的上下文切换,消耗一定的性能。
-
优先级高的等待优先级低的释放锁,会导致优先级倒置,引起性能问题。
5.1 synchronized的两种用法
5.1.1 synchronized 方法 也叫同步方法
例如:public synchronized void method(int a){}
这个是控制对“对象”的访问,每个对象对应一把锁,也就是说,当你需要执行method的这个方法时,你需要拿到调用该方法的对象的锁,才行不然是不能执行的。
这个有一个很大的缺点:当你将一个大的方法加了这个synchronized之后,会大大的影响效率?有人可能会问为什么,因为当你的方法里面有一些只需要读的数据,那你还把它锁,其他线程也拿不到,只能等当前正在访问的线程结束后才能拿到岂不是太影响效率了。
补充:synchronized默认的是this这个对象。
5.1.2 synchronized 块 也叫同步块
例如: synchronized (Object){ } ---->Object 称之为同步监视器
- Object 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身,或者是class
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码.
- 第二个线程访问,发现同步监视器被锁定,无法访问.
- 第一个线程访问完毕,解锁同步监视器.
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问.
补充一下:打个比方就是说,银行取钱这个案例,有两个类,Bank类、Account类。你在run()方法里进行扣钱的这个操作,如果说你在run()方法前加了一个synchronized那这个就是锁的Bank这个对象,那是没有用的数据的操作是你的的Account类所以要锁这个对象,就要用同步块来锁。
class Bank implements Runnable{
@override
public void run(){
//扣钱操作
}
}
class Account{
}
5.死锁
定义:多个线程互相拿着对方需要的资源不放,僵持在那里。
例如下示例,就是死锁。
死锁产生的4个必要条件:
- 互斥条件:一个资源只能被一个进程占用
- 不可剥夺条件:某个进程占用了资源,不能强行剥离。
- 请求和保持条件:一个进程因请求资源而阻塞,对已经获得资源不释放。
- 循环等待条件:一定会有一个环互相等待。
6.Lock锁
这里补充一个这个Lock锁,先看下这个和synchronized的一个对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁, 出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用L ock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序: Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方法体之外)
具体来看下简单的案例怎么用的:
package 线程的三种创建方式.juc;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock implements Runnable{
private int tickenum=10;
private final ReentrantLock reentrantLock=new ReentrantLock();
public void run() {
while (true) {
//上锁
reentrantLock.lock();
if (tickenum > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "抢到了第" + tickenum-- + "票");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//放锁
reentrantLock.unlock();
}
} else {
break;
}
}
}
public static void main(String[] args) {
TestLock test02 = new TestLock();
new Thread(test02,"小明").start();
new Thread(test02,"黄牛").start();
}
}
7. 多线程写协作
重点来了这个很重要的,线程之间怎么协作呢?首先要搞清楚消费者跟生产者的问题。
7.1 消费者和生产者问题
通俗点说实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。
但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
- 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
- 如果共享数据区为空的话,阻塞消费者继续消费数据;
7.2 实现生产者消费者问题的方法
这里例举3种,但是只简单介绍一下第一种了,后面2种先不介绍了,哎 ,码字太累了下次写。
1.使用Object的wait/notify的消息通知机制;
2.使用Lock的Condition的await/signal的消息通知机制;
3.使用BlockingQueue实现。
说第一种之前先补个东西
方法 | 说明 |
---|---|
sleep() | 这个不会释放会抱着锁睡觉 |
wait() | 这个会释放锁 |
7.2.1 使用Object的wait/notify的消息通知机制
并发协作模型“生产者 / 消费者模式 ” —>管程法
- 生产者 : 负责生产数据的模块(可能是方法、对象、线程、进程)
- 消费者 : 负责处理数据的模块(可能是方法、对象、线程、进程)
- 缓冲区 : 消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
- 生产者将生产好的数据放入缓冲区,消费者从里面拿。
并发协作模型“生产者 / 消费者模式 ” —>信号灯法
管程法的案例展示:
package 管程法;
//管程法--》利用缓冲区解决
public class Test {
public static void main(String[] args) {
Box box=new Box();
new Productor(box).start();
new Consumer(box).start();
}
}
class Productor extends Thread{
Box box;
public Productor(Box box) {
this.box = box;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
box.put(new Duck(i));
System.out.println("生产了"+i+"只鸭子");
}
}
}
class Consumer extends Thread{
Box box;
public Consumer(Box box) {
this.box = box;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了"+box.get().id+"只鸭子");
}
}
}
class Duck{
int id;//编号
public Duck(int id) {
this.id = id;
}
}
//容器
class Box{
//容器大小
Duck[] ducks=new Duck[10];
//容器计数器
int count=0;
//生产者放入产品
public synchronized void put(Duck duck){
//如果满了,就需要等待消费者消费
if(count==ducks.length){
//通知消费者消费,让生产者等待
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果没有满,就放入产品
ducks[count]=duck;
count++;
//可以通知消费者消费了
this.notifyAll();
}
//消费者消费产品
public synchronized Duck get(){
//判断能否消费
if(count==0){
//等待生产者生产,消费者等待
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果可以消费
count--;
Duck duck=ducks[count];
//消费完了,告诉生产者生产
this.notifyAll();
return duck;
}
}