操作系统定义
操作系统是一个管理软件,对下管理硬件设备,对上给软件提供稳定的运行环境
冯诺依曼体系结构:CPU中央处理器(运算器+控制器),存储结构,输入设备(键盘,鼠标,输出设备(显示器,音响)
进程和线程定义
进程:(任务资源管理器)一个跑起来的程序,就叫进程,进程是操作系统资源分配的基本单位,进程可以理解为一个应用程序程序执行过程,应用程序一旦执行,就是一个进程,每个进程都有自己独立的地址空间,每次启动一个进程,系统就会为它分配地址空间。
线程:线程被包含在进程中,一个进程默认有一个线程也可以有多个,每一个线程都是一个执行流,可以单独在CPU上进行调度。同一个进程中的·线程公用一份系统资源(内存+文件),所以线程也叫做轻量级进程。线程是调度执行的基本单位
进程在操作系统中(组织+描述)组织:通过双向链表把PCB串在一起 描述:(pid:身份标识符,内存指针:指向说内存是哪些 ,文件描述符表:硬盘上文件等其他资源)
通过一组PCB(进程控制块)来描述一个进程,每个PCB对应一个线程,一组进程上的内存指针和文件描述符表只有一份,而状态,优先级,上下文则是每个线程都有一份
进程和线程区别
1.进程是系统资源分配的基本单位,线程是调度执行的基本单位
2.不同进程都有自己独立的内存空间,同一个进程所有线程共享本进程的地址空间1
3.同一个进程所有线程共享本进程资源,进程之间是资源独立
4.线程创建和销毁开销小,可以提高效率
5.线程之间会相互干扰,进程之间不会(当线程数目达到极限,CPU核心就会被吃满,如果某个线程发生意外,很可能整个进程会被带走)
线程的创建方式
-
继承Thread类
public class MyThread extends Thread{//Thread相当于对操作系统中线程进行封装
@Override
public void run(){//重写run,run是Thread父类里面已经有的方法,run里面的逻辑就是这个线程要执行的工作
System.out.println("重写run");
}
public static void main(String[] args) {
MyThread myThread=new MyThread();//创建一个实例,并不是在系统中真创建
myThread.start();//调用start方法时,才真正的创建了一个线程
}
}
运行一个Java程序,就是启动一个进程,一个进程里面至少会含有一个线程,main方法所在的线程叫做主线程
main主线程和MyThread创建出的线程都是并发执行的关系
myThread.start另外启动一个线程来执行run方法,新线程是一个单独的执行流,和现有的线程执行流不想关,并发执行
-
实现Runable接口
class MyRunnableer implements Runnable{
@Override
public void run(){
System.out.println("重写run方法");
}
}
public class MyRunnable {
public static void main(String[] args) {
MyRunnableer myRunnableer=new MyRunnableer();
Thread t=new Thread(myRunnableer);
t.start();
}
}
把线程干的活与线程本身分开,使用myRunnableeer来表示线程要完成的工作,把任务提取出来,目的是为了解耦合
继承Thread写法把线程要完成的工作和线程本身耦合在一起,如果对代码改动比较大,myRunnableeer只需要把它传给其他实体即可
-
使用匿名内部类,实现创建Thread子类方式
public static void main(String[] args) {
Thread t=new Thread(){ //创建Thread子类,同时实例化出一个对象
@Override
public void run() {
System.out.println("重写run方法");
}
};
t.start();
}
-
使用匿名内部类完成Runnable
public static void main(String[] args) {
Thread t=new Thread(new Runnable() { //匿名内部类实例作为构造方法参数
@Override
public void run() {
System.out.println("重写run");
}
});
t.start();
}
-
使用lambda表达式
public static void main(String[] args) {
Thread t4 = new Thread(() -> {
System.out.println("任务");
});
lambda本质上是匿名函数,()是函数形参 {}是函数体 ->特殊语法
线程的属性
-
ID
这个是Java中给Thread对象安排的身份标识符,身份标识符可以有多个,不同环境下使用不同标识符 getid()
-
isDaemon (是不是守护线程) setDaemon()设置成后台线程,必须在start之前设置
默认创建线程是前台线程,前台线程会阻止进程退出,如果main运行完,前台线程还未完成,进程不会退出,如果是后台线程,则相反
例:转账,必须是前台线程,必须等到所有转账都转完才结束,十分精准
微信运动步数,不精准,后台线程
-
isAlive (内核线程是否存活)
Thread对象虽然和内核线程是一 一对应关系,但是生命周期并不是完全相同,Thread对象创建出来1,内核线程不一定会有,调用start方法内核线程才会有
当内核线程(run)执行完,内核线程也就销毁了,但Thread对象还在(Thread对象生命周期比线程长)
-
start() ,调用start方法才会创建线程
直接调用run并没有创建线程,只是在原来的线程中运行代码
调用start则是创建了线,在新线程中执行代码,和原来线程并发执行
-
线程中断 本质上是让run方法尽快结束,而不是run执行一半强制结束
1.定义一个标志位,作为线程是否结束的标记
2.使用标准库自带的标准位
interrupt方法行为有两种情况:
1.线程在运行状态,会设置 Thread.currentThread().isInterrupted()
2.线程阻塞状态,interrupt会设置标志位,但是sleep/wait这些阻塞方法会清除这个标志位,所以看起来好像没设置,触发interruptException
在Java中,中断线程不是强制性的,可以由代码本身来决定是立即结束 、还是不理会 、还是稍后处理
public class ThreadDemo {
private static class MyRunnable implements Runnable{
@Override
public void run(){
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"1111");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+"2222");
break;
}
}
System.out.println(Thread.currentThread().getName()+"3333");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable=new MyRunnable();
Thread thread=new Thread(myRunnable,"李四");
System.out.println(Thread.currentThread().getName()+"李四开始转账");
thread.start();
Thread.sleep(10*1000);
System.out.println(Thread.currentThread().getName()+"李四是骗子");
thread.interrupt();
}
}
-
线程等待(线程之间的调度顺序是不确定的,在main中调用t.jion方法,让main阻塞等待,等执行到t执行完,main才可以继续执行
public void join(long millis, int nanos) 带时间版本,等待但不是无限等待
-
获取当前线程引用 (哪个线程调用,得到的就是哪个线程引用)
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread(); 哪个线程调用,得到的就是哪个线程引用
System.out.println(thread.getName()); } }
线程状态
-
NEW: 安排了工作, 还未开始行动(Thread对象创建出来,内核PCB还未创建,没有真正创建线程)
-
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.(就绪状态)
-
BLOCKED: 这几个都表示排队等着其他事情,等待锁的时候进入阻塞状态
-
WAITING: 这几个都表示排队等着其他事情,特殊阻塞,调用wait
-
TIMED_WAITING: 按照一定时间进行阻塞
线程安全
-
定义:在多线程各种随机调度顺序下,代码没有bug,都能符合预期执行,这样的代码是线程安全的。
-
线程不安全原因
1.抢占式随机执行 (每个线程调度执行的过程可视为全随机的)
2.多个线程修改同一个变量(string是不可变对象,不能修改string对象内容)
3.修改操作不是原子的(原子表示不可分割的最小单位)
例:count++操作(三条指令在CPU上完成)
(1)把内存数据读取到cpu寄存器中
(2)把cpu寄存器的值进行+1
(3) 把寄存器的值放入内存中
cpu执行指令都是以一个指令为单位去执行,不能说指令执行一半就完成线程调度,int赋值则是更安全点
4.内存可见性 可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到(Jvm代码优化引入Bug)
while(counter.count==0){ } 每次执行load,并且load结果还不一样,干脆只读一次,后面不读了(编译器优化) 解决:编译器自己判断不准的,把不该优化的进行了优化需要让程序员显示提醒编译器,这个地方要不要优化volatile
5.指令重排序
解决线程不安全
-
加锁(通过特殊手段,让代码变成原子的)
例:在count++之前加锁,count++之后在解锁,这样别的线程只能阻塞等待
public synchronized void increase(){
count++;
}
synchronized修饰方法,当进入这个方法就会加锁,方法执行完毕自然解锁
-
解决内存可见性问题(volatile)
volatile可以使用这个关键字来修饰一个变量,被修饰的变量,编译器不会出现只读寄存器不读内存的优化
volatile可保证内存可见性,但不保证原子性(只能针对一个线程读,一个线程使用的情况)
cpu操作寄存器比操作内存快千倍,导致操作内存是一个不明智的选择,重复读内存,不需要真的读内存,只读一次内存,后续读缓存中的数据即可(编译器优化)
代码在写入 volatile 修饰的变量的时候, 改变线程工作内存(寄存器+缓存)中volatile变量副本的值 ,将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候, 从主内存中读取volatile变量的最新值到线程的工作内存中 ,从工作内存中读取volatile变量的副本
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
import java.util.Scanner;
public class Counter {
static class Counter1{
public volatile int a=0; 如果不加volitail,t1读的是工作内存内容,t2对flag变量进行修改,t1也不知道数据是谁,不能立即结束循环
}
public static void main(String[] args) {
Counter1 counter1=new Counter1();
Thread t1=new Thread(() ->{
while (counter1.a==0){
}
System.out.println("循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("输入一个整数");
counter1.a= scanner.nextInt();
});
t1.start();
t2.start();
}
synchronized的几种用法
1.修饰方法(直接修饰方法,相当于锁对象就是this)
public class SynchronizedDemo {
public synchronized void methond() {
} }
2.修饰代码块(有些代码要加锁,有些不需要加锁)要判断锁对象
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
} } }
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (class) {
} } }
3.修饰静态方法
public class SynchronizedDemo {
public synchronized static void method() {
} }
1.加锁要考虑好锁哪段代码,锁的范围不一样,代码的执行效果也不一样
2.不是加锁一定安全,而是通过加锁让并发修改1同一个变量改成串行修改同一个变量
3.如果一个线程上锁,一个没有,也就不存在锁竞争,也就不会阻塞等待,也就不会并发修改变成串行修改
3.在类里面设置静态成员变量,类属性是唯一的,类对象也是唯一的,尽管count和count1是两个实例,但这个静态成员变量其实是一个,会产生锁竞争
-
synchronized的特性
1.互斥性(synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,如果其他线程也执行到同一个对象,synchroonized就会阻塞等待)
2.可重入(synchronized不会出现两次加锁锁死的情况)
不可重入锁:第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁.
可重入锁:
public class Counter { public int count=0; synchronized void increase(){ count++; } synchronized void increase2(){ increase(); } } synchromized(this){ //真正加锁 synchronized(this){ //直接放行 } //执行到这里不用真解锁 } 引用一个计数器,加锁++ 解锁就-- 若计数器为0,加锁操作是真加锁
increase和increase2都加锁,increase是针对当前对象进行加锁,调用increase2的时候先加了一次锁,执行到increase又加了一次锁,相当于连续加了两次锁
可重入锁底层实现是非常简单的,只要让锁记录好是哪个线程持有的这把锁就好
t线程尝试针对this来加锁,this这个锁就记录了是t线程有的它,第二次进行加锁时,锁看见还是t线程就直接通过了,不会阻塞
可重入锁:1.让锁里面持有线程对象,记录谁加了锁
2.用计数器来判断啥时候加锁和解锁
-
wait和notify用来调配线程执行顺序
wait, notify, notifyAll 都是 Object 类的方法,Object是所有类的老大,因此可以使用任意类进行实例化,都可以调用wait方法
1.线程执行到wait就会阻塞,直到另一个线程调用notify,才可以把这个wait唤醒,继续执行
2.wait操作本质上做3件事:
释放当前锁 (释放锁就是给别的线程机会来拿到锁)
进行等待通知(前提一定是先释放锁)
满足一定条件的时候(别人调用notify)被唤醒,然后尝试重新获取锁
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
public static void main(String[] args) throws InterruptedException { Object object=new Object(); synchronized (object){ System.out.println("等待中"); object.wait(); 程序会一直等下去,需要notify唤醒 System.out.println("等待结束"); } }
1.notify也要包含在synchronized里面,线程1没有释放锁,线程2无法调用notify(因为阻塞等待),线程1调用wait,释放了锁,线程1代码阻塞在synchronized里面,但此时锁是释放状态,线程2可以拿到锁
public class Counter { static class Counter1{ public volatile int a=0; } static class waitTask implements Runnable{ private Object locker; public waitTask(Object locker){ this.locker=locker; } @Override public void run(){ synchronized (locker){ try { System.out.println("wait开始"); locker.wait(); System.out.println("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class notifyTask implements Runnable{ private Object locker; public notifyTask(Object locker){ this.locker=locker; } @Override public void run(){ synchronized (locker){ try { System.out.println("notify开始"); locker.wait(); System.out.println("notify结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Object locker=new Object(); Thread t1=new Thread(new waitTask(locker)); Thread t2=new Thread(new notifyTask(locker)); t1.start(); Thread.sleep(2000); t2.start(); } public static void main2(String[] args) throws InterruptedException { Object object=new Object(); synchronized (object){ System.out.println("等待中"); object.wait(); System.out.println("等待结束"); } }
notifyall唤醒所有的线程
public class Counter { static class Counter1{ public volatile int a=0; } static class waitTask implements Runnable{ private Object locker; public waitTask(Object locker){ this.locker=locker; } @Override public void run(){ synchronized (locker){ try { System.out.println("wait开始"); locker.wait(); System.out.println("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class notifyTask implements Runnable{ private Object locker; public notifyTask(Object locker){ this.locker=locker; } @Override public void run(){ synchronized (locker){ System.out.println("notify开始"); locker.notifyAll(); System.out.println("notify结束"); } } } public static void main(String[] args) throws InterruptedException { Object locker=new Object(); Thread t1=new Thread(new waitTask(locker)); Thread t2=new Thread(new notifyTask(locker)); Thread t3=new Thread(new notifyTask(locker)); Thread t4=new Thread(new notifyTask(locker)); Thread t5=new Thread(new notifyTask(locker)); t1.start(); t5.start(); t3.start(); t4.start(); Thread.sleep(1000); t2.start(); }
虽然唤醒所有线程,这些线程需要锁竞争
wait和sleep的区别(面试题)
wait需要搭配synchronized使用,sleep不需要
wait是object的方法,sleep是Thread的静态方法
相同点:都可以让线程放弃执行一段时间
-
多线程的几种设计模式
-
单例模式 (单个实例、对象)这个是需求决定的,有些场景要求实例不能有多个,本质上就是借助编程语言的自身语法特性,强行限制某个类,不能创建多个实例
饿汉模式
解决方案:static名义上是静态,实际上起到的效果和名字无任何关系
static修饰成员/属性,变成类成员/类属性,当属性变成类对象属性,此时已经是单个实例(类对象通过JVM加载(.class)文件,而此时类对象,其实在JVM中也是实例,JVM对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的成员static修饰也只有一份)
class Singleton{
private static Singleton instance=new Singleton(); //这个singleton这个类唯一实例,类加载时创建实例
private Singleton(){} //把构造方法设为private,此时类外无法new实例
public static Singleton getInstance(){ //拿到singleton实例需要借助getInstaance
return instance;
}
}
懒汉模式
创建实例更加迟,带来更高效率
class Singleton1{
private static Singleton1 insatnce=null;
private Singleton1(){}
public static Singleton1 getInstance(){
if (insatnce==null){
insatnce=new Singleton1(); 真正创建实例
}
return insatnce;
}
}
这个创建实例是不安全的,如果多个线程同时调用getInsatance方法,就可能创建多个实例
进行优化
class Singleton1{
private volatile static Singleton1 insatnce=null;//禁止指令重排序和内存可见性
private Singleton1(){}
public static synchronized Singleton1 getInstance(){ //加锁变成原子操作,t2读的是t1修改后的值
if (insatnce==null){ //判断是否需要加锁,因为线程加锁开销比较大,线程不安全在实例创建之前,需要加锁,实例创建之后不加
synchronized (Singleton1.class){
if(instance==null){ //判断是否要创建实例
insatnce=new Singleton1();
}
}
return insatnce;
}
}
1.双重if判定,第一个if判断是否需要加锁(加锁开销比较大,线程不安全只存在与实例创建之前) 第二个if是判断是否需要创建实例
2.假设2个线程同时调用getInsatnce,第一个线程拿到锁,进入第二层if,开始new对象(1.申请内存,得到内存首地址2.调用构造方法,构造实例 3.把内存首地址赋给instance引用),这个场景可能会造成指令重排序,t1执行了1和3之后(得到不完全对象,只是有内存,内存数据无效),执行2在前
-
阻塞对列 (符合先进先出)
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
应用场景:生产者消费者模型 (多线程协同工作一种方式)
优点:1.阻塞队列有利于代码解耦合(明确分工,之间并没有过多交集)
2.相当于缓冲区,平衡了消费者和生产者处理能力(当流量暴增A和队列承受压力,B和C还是按原来节奏消费数据,缓解直接冲击)
-
线程池
1.为什么从线程池里面直接取比创建新线程快?
创建线程需要在操作系统内核中实现,涉及用户态---内核态切换操作,存在开销,从线程池里面取、放只涉及用户态,用户每个进程都是自己执行逻辑,效率更高
2.Java标准库直接写好的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
此处创建线程池没有显示new而是通过executors静态方法来完成,这样叫做工厂模式
Java线程池本体是ThreadPoolExactor,构造方法麻烦,简化构造方法,标准库提供一系列工厂方法
3.线程池使用
public static void main(String[] args) {
ExecutorService pool= Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("helll0");
}
});
}
4.线程池实现
1.一个线程池的可以同时提交n个任务,对应线程池中有m个线程负责n个任务(生产者消费者模型,先搞一个阻塞对列,每一个任务放在阻塞队列里面,m个线程从队列里面取元素)
-
锁策略
乐观锁:预测锁冲突概率不高(在数据提交更新时才会对数据是否产生并发冲突去检测)
悲观锁:预测锁冲突概率较高(每次拿数据都会上锁)
普通互斥锁:synchronized当两个线程竞争同一把锁,就会产生等待
读写锁:把读操作和写操作区分开来(两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.;两个线程都要写一个数据, 有线程安全问题;. 一个线程读另外一个线程写, 也有线程安全问题.)
读写锁相比普通互斥锁,少了锁竞争,优化效率
Synchronized 不是读写锁
轻量级锁:加锁解锁开销比较小(纯用户态加锁,线程池)
重量级锁:加锁解锁开销比较大(进入内核态加锁,开销大)
synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.
自旋锁:轻量级锁(如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会 在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.)
挂起等待锁:重量级锁
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的.
公平锁:有先来后去顺序
非公平锁:不管先来后到顺序
操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
synchronized 是非公平锁
可重入锁:针对同一个线程同一把锁,连续加锁两次,不会出现死锁情况
不可重入锁: 针对同一个线程同一把锁,连续加锁两次,会出现死锁情况
synchronized关键字锁都是可重入的
-
CAS
1.定义: 把内存中的某个值和CPU寄存器的值进行交换(如果两个值相同,就把另一个寄存器B中的值和内存值进行交换,把内存值放到寄存器B,同时B值写给内存),一系列操作是通过一个CPU指令完成(原子的),不仅线程安全还高效。
2.应用场景
1.实现原子类(CAS 是直接读写内存的, 而不是操作寄存器. CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的)
2.实现自旋锁
-
CAS的ABA问题
1.在CAS中进行比较时,发现寄存器A和内存M值相同,无法判断M是不是变了又变回来,还是始终都没变
2.大部分情况反复改变是不影响,但也有特殊情况(ABA)扣款两次
3.解决方案:
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1. 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
-
Synchronized原理
1.Synchronized是加锁,当两个线程针对同一个对象加锁时,会出现锁竞争,之后另一个线程就得阻塞等待,直到这个线程释放锁
2.synchronized的加锁过程
- 偏向锁(类似与懒汉模式,必要时加锁,能不加就不加,偏向锁不是真的加锁,而是只设置了一个状态,产生锁竞争才会加锁)
- 轻量级锁(随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应自旋锁)通过CAS来实现,通过 CAS 检查并更新一块内存 (比如 null => 该线程引用) 如果更新成功, 则认为加锁成功 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU),但此处的自旋不会一直持续进行,而是达到一定时间、重复次数就不在自旋,也就是所谓的自适应)
- 重量级锁(如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁)(执行加锁操作, 先进入内核态. 在内核态判定当前锁是否已经被占用 如果该锁没有占用, 则加锁成功, 并切换回用户态. 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒. 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁.)
-
Callable(是一个接口,描述任务带返回值)
Callable 和 Runnable 相对, 都是描述一个 "任务".
Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定. FutureTask 就可以负责这个等待结果出来的工作
//创建线程计算1+。。。。100的返回值
static class Result{
public int sum=0;
public Object lock=new Object();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer>callable=new Callable<Integer>() { //写一个匿名内部类实现callable接口
@Override
public Integer call() throws Exception { //重写callable的call方法完成累加
int sum=0;
for (int i = 1; i <=1000 ; i++) {
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futureTask=new FutureTask<>(callable); //把Callable实例使用FutureTask包装一下
Thread t=new Thread(futureTask);//创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
t.start();
int result=futureTask.get();//在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
System.out.println(result);
}
-
JUC(java.util.concurrent的常见类)
1.ReentrantLock (对synchronized的补充)
lock.lock();
try {
// working
} finally {
lock.unlock()
}
优势:
1.trylock提供更多的可能(试试看可以加锁,成功则加锁成功,失败则加锁失败,并且还可以指定加锁等待超时时间
2.synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式 RuntrantLock locker=new Runtrantlock(true)
3.. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指 定的线程
4.synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, 在 JVM 外实现的(基于Java实现的)
2.Semaphore(信号量)用来表示 "可用资源的个数". 本质上就是一个计数器
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(4); 初始化为4,表示有4个可用资源
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire(); acquire方法申请资源p,可用资源减一
System.out.println("我获取到资源");
Thread.sleep(1000);
System.out.println("释放资源");
semaphore.release(); release释放资源,可用资源+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) { 创建20个线程,每个线程尝试申请资源,sleep1秒后,释放资源
Thread t=new Thread(runnable);
t.start();
}
3.CountDown Latch(类似于比赛)使用它先设置有几个选手,每个选手撞线调用此方法,当撞线次数达到选手个数就比赛结束
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(10);//表示有10个任务·
Runnable r=new Runnable() {
@Override
public void run() {
Thread.sleep(Math.random(10000));
latch.countDown(); //每个任务执行完毕都调用countDown,在CountDownLatch内部计数器同时自减
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
//必须等10人全完成任务
latch.await(); //阻塞等待所有任务执行完毕,计数器为0
System.out.println("比赛结束");
}
4.线程池
理解 ThreadPoolExecutor 构造方法的参数:
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.
corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退). keepAliveTime: 临时工允许的空闲时间.
unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
workQueue: 传递任务的阻塞队列 threadFactory: 创建线程的工厂, 参与具体的创建线程工作. RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理. AbortPolicy(): 超过负荷, 直接抛出异常.
CallerRunsPolicy(): 调用者负责处理
DiscardOldestPolicy(): 丢弃队列中最老的任务.
DiscardPolicy(): 丢弃新来的任务.
-
死锁
1.一个线程一把锁,线程连续加锁两次,如果这个锁是不可重入锁,则是死锁
2.两个线程两把锁 (锁套锁)
3.两人分别拿不同的东西不交换
4.多个线程多把锁,更容易死锁(哲学家就餐)
死锁的4个必要条件:
1.互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2.不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3.请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4.循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路
解决:给锁编号,给定两把锁之间必须先取编号小的,后取大的,
锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号 (1, 2, 3...M). N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.
public static void main(String[] args) {
Object lock1=new Object();
Object lock2=new Object();
Thread t1=new Thread(){
@Override
public void run(){
synchronized (lock1){
synchronized (lock2){
}
}
}
};
t1.start();
Thread t2=new Thread(){
@Override
public void run(){
synchronized (lock1){
synchronized (lock2){
}
}
}
};
t2.start();
}