线程学习笔记
Java线程机制:抢占式,调度机会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都分配到合理的时间驱动任务
两个概念:同步排队,异步同时。
并发:多个事件在同一指定时间段内发生
并行:强调同一时刻,同时开始。
线程调度:分时;抢占式
进程和线程的区别:
进程:资源分配的基本单位
线程:调度的基本单位
JVM模拟CPU空间线程,但在OS看来他和其他APP无异,JVM调用的线程和OS的内核线程的对应关系就叫做线程模型,hotspot模型1:1
多线程实现方法:
1) 继承Thread类Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把
run()方法称为线程执行体。 - 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
在主函数中声明:
线程类名 m = new 线程类名();
m.start();//调用
在线程类定义中继承Thread
,public void run(){定义行为}
每个线程都有自己的栈空间
匿名内部类实现:
new Thread(){
public void run(){
for(int i=0;i<10;i++){
System.out.print("abcde"+i);
}
}
}.start();
2)继承Runnable接口创建(任务)
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
实现Runnable与继承Thread相比优势:
- 通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况
- 可以避免单继承的局限
- 任务与线程分离,传递不同的实现类实现不同的任务,提高了程序的健壮性
- 后续的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
tips:
- Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。 而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
- 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
Thread类构造方法:Thread(任务runnable,(名称))或 Thread(名称)
更改Thread名称:setName().
或者在自定义类中调用父类带参构造super(name).
获取当前正在执行的线程:Thread.currentThread().
线程的休眠:静态方法Thread.sleep(mills)
线程中断: t.interrupt();
(在catch块中return;中断任务)
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
return;
}
}
}
用户线程:当一个线程不含任何存活的用户线程时,进程结束
守护(后台)线程:用于守护用户的线程,最后一个用户线程结束随之结束
设置守护线程:
Thread t = new Thread(new SimpleDeamons())
t.setDaemin(true);
线程不安全解决:
1) 同步代码块(隐式锁):
synchronized(锁对象){线程不安权代码}
锁对象定义在run()
外;注意是同一把锁,最多允许一个线程拥有同步锁,出了同步代码块才归还锁,其他没拿到锁的线程阻塞,但频繁的获取锁,判断锁,释放锁,效率低。
2) 同步方法(隐式锁):给冲突的方法添加synchronized
修饰符
如果是静态方法:锁是 Ticket.Class
;动态:锁是 this
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
注:下列写法是创建了三个对象,三把锁,无法解决线程不安全问题
Runnable run = new Ticket();
new Thread(New Ticket()).start();
new Thread(New Ticket()).start();
new Thread(New Ticket()).start();
3) 显示锁Lock(接口): 子类ReentrantLock
Lock l = new ReentrantLock;
l.lock(); { }
l.unlock();
显示锁和隐式锁的区别:
1)
Sync:是针对一个对象的。Sync是Java中的关键字,是由JVM来维护的。是JVM层面的锁。sync是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的。
Lock:代码块层面的锁定。JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁
2)等待是否可中断
Sync:是不可中断的。除非抛出异常或者正常运行完成
Lock:可以中断的。中断方式:
a:调用设置超时方法tryLock(long timeout ,timeUnit unit)
b:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
3)加锁的时候是否可以公平
Sync:非公平锁
lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。
true:公平锁(排队)
false:非公平锁(抢占)
4)锁绑定多个条件来condition
Sync:没有。要么随机唤醒一个线程;要么是唤醒所有等待的线程。
Lock:用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像sync那样,不能精确唤醒线程。
5) 使用锁的方式比较
Sync:原始采用Cpu悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在cpu抓换线程阻塞时会引起线程的上下文切换,当有很多线程竞争锁时,会引起cpu频繁的上下文切换导致效率很低。
Lock:采用乐观锁方式。乐观锁即每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就去重试,直到成功。乐观锁的机制就是CAS操作(Compare and Swap)。ReentrantLock源码中比较重要获得锁的一个方法是compareAndSetState(),采用了非阻塞算法,即一个线程的失败或者挂起不应该影响其他纤层的失败或者挂起算法。
多线程通信:(生产者与消费者问题)
利用标志位使每一个线程唤醒Obj.notifyAll()
;和休眠Obj.wait()
交替执行解决
//生产者与消费者案例
//只给两个线程加同步锁也不管用,会出现连续打印抢占,光加线程安全解决不了问题:线程回首掏
public class WaitAndNotify {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndTaste("老干妈小米粥","香辣味");
}else {
f.setNameAndTaste("煎饼果子","酸甜口");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
static class Food{
private String name;
private String taste;
//true表示可以生产
private boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//服务员
public synchronized void get(){
if(!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程6状态关系:
第三种线程实现方式Callable
作用:可以实现主线程等待,直到Callable执行完毕返回值
使用步骤:
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
- 主线程创建Callable c对象,创建FutureTask对象并传入第一步编写的Callable类对象
FutureTask<T> future = new FutureTask<>(callable c);
- 通过Thread,启动线程
new Thread(future).start();
主线程调用futureTask.get()
获取返回值,会等待子线程的返回值后再执行
Runnable 与 Callable的相同点
1.都是接口
2.都可以编写多线程程序
3.都采用Thread.start()启动线程
Runnable 与 Callable的不同点
1.Runnable没有返回值;Callable可以返回执行结果
2.Callable接口的call()允许抛出异常;Runnable的run()不能抛出
3.Callable获取返回值
线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
2) 定长线程池
3) 单程线程池
Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一
时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。