线程基础,线程之间的共享与协作
目录
5.线程中的stop(),interrupt(),isInterrupted(),static方法interrupted()方法的含义和区别
3.Yield(),sleep(),wait(),notify(),notifyAll()含义
1.CPU时间片轮转机制
时间片轮转法(Round-Robin,RR)主要用于分时系统中的进程调度。为了实现轮转调度,系统把所有就绪进程按先入先出的原则排成一个队列。新来的进程加到就绪队列末尾。每当执行进程调度时,进程调度程序总是选出就绪队列的队首进程,让它在CPU上运行一个时间片的时间。时间片是一个小的时间单位,通常为10~100ms数量级。当进程用完分给它的时间片后,系统的计时器发出时钟中断,调度程序便停止该进程的运行,把它放入就绪队列的末尾;然后,把CPU分给就绪队列的队首进程,同样也让它运行一个时间片,如此往复.
2.进程和线程
线程,有时被称为轻量级进程,是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
简单来说,
进程:程序运行资源分配得最小单位,进程内部有多个线程,线程共享这个进程上的资源。
3.并行和并发
并行:同一时刻,可以处理事件得能力
并发:与时间单位相关,在单位时间内可以处理事情得能力。
例:食堂打饭,有8个窗口,设每人打饭时常为20秒,则并行量为8,并发量为每分钟24
4.高并发编程的含义,好处及注意事项
含义:高并发是请求,指的是多个客户端同一时刻向服务端发送请求,它是一种现象。
比如,电商网站在双11凌晨12:00分 同时有2000个下单请求。
多线程是处理,指的是同一时刻多个执行者处理同一类的任务,它有具体的实现。比如电商网站在双11凌晨12:00分同时有100个线程处理2000个下单请求
好处:
1.更好得利用系统资源
2.提高程序的运行速度
注意事项:
1.安全性问题,多个线程共享数据可能产生与期望不符合的结果
2.活跃性问题,当某个操作无法继续进行下去时,就会产生活跃性问题,如死锁,饥饿,活锁等问题
3 性能问题
a.线程过多时会使得CPU频繁切换,花在调度上时间太多。
b.多线程环境必须使用同步机制,导致很多编译器想做的优化被抑制。
c.线程过多还会消耗过多内存。
(二)认识java的线程
1.3种创建线程的方式
1、Java使用Thread类代表线程。
所有的线程对象必须是Thread类或其子类的实例。
当线程继承Thread类时,直接使用this即可获取当前线程,Thread对象的getName()方法返回当前线程的名字,因此可以直接调用getName()方法返回当前线程的名字。
Thread.currentThread():该方法总是返回当前正在执行的线程对象。
2、创建线程方式1:继承Thread类创建线程类
这种方式创建线程的步骤一般为:
1》定义Thread类的子类,并重写该类的run()方法,该方法作为线程的线程执行体。
2》创建Thread子类的实例,即线程对象。
3》调用线程对象的start()方法启动线程。
public class extendsThread extends Thread{
@Override
public void run(){
...
//do something
System.out.println(this.getName()); }
public static void main(String[] agrs){
//创建并启动第一个线程
new extendsThread().start();
//创建并启动第二个线程
new extendsThread().start();
}
}
创建线程方式2:实现Runnable接口
这种方式创建线程的步骤一般为:
1》定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2》创建Runnable接口实现类的实例,并将该实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。本质是Thread对象负责执行实现类对象的run()方法体。
3》调用线程对象的start()方法来启动该线程。
public class ImplRunnable implements Runnable{
@Override
public void run(){
//do something
System.out.println(Thread.currentThread().getName());
}
public static void main(String agrs){
System.out.println(Thread.currentThread().getName());
ImplRunnable target=new ImplRunnable();
//创建并启动第一个线程
new Thread(target,"Thread1").start();
//创建并启动第二个线程
new Thread(target,"Thread2").start();
}
}
创建线程方式3:使用Callable和Future创建线程,创建有返回值的线程
这种方式创建线程的步骤一般为:
1》创建Callable接口的实现类,并实现call()方法,该方法作为线程执行体,且该方法有返回值。然后创建该实现类的实例。
2》使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3》使用FutureTask对象作为Thread对象的target创建并启动新线程。
4》调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
public class ImplCallable implements Callable<Integer>{
@Override
public Integer call(){
//do something
System.out.println(Thread.currentThread().getName());
//return a value.
return 1;
}
public static void main(String[] agrs) throws InterruptedException, ExecutionException{
...
//创建并启动一个线程
FutureTask<Integer> target=new FutureTask<Integer>(new ImplCallable());
new Thread(target,"Thread3 with value.").start();
//获取返回值
System.out.println(target.get());
}
}
2.线程安全停止工作的方式
a.自然停止,线程中的代码正常运行完毕。
b.线程内抛出异常
注意:stop(),resume(),suspend()方法会停止线程,但不会释放该线程所占用的资源
3.线程常用方法和线程的状态
常用方法:
run():相当于线程的任务处理逻辑的入口方法,它由Java虚拟机在运行相应线程时直接调用,而不是由应用代码进行调用。简单来说就是调用一个普通方法
srtart():启动相应的线程。启动一个线程实际是请求Java虚拟机运行相应的线程,而这个线程何时能够运行是由线程调度器决定的。start()调用结束并不表示相应线程已经开始运行,这个线程可能稍后运行,也可能永远也不会运行。
yield():将线程从运行状态转为就绪状态
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
- 用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止
- 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器)
注意:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑
守护线程和他的主线程共死,守护线程内的finally内得代码不一定执行
5.线程中的stop(),interrupt(),isInterrupted(),static方法interrupted()方法的含义和区别
stop():暴力中断线程,会导致线程资源未释放
interrupt():中断线程。调用该方法的线程的状态将被置为"中断"状态。注意:线程中断仅仅是设置线程的中断状 态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。(如果线程在wait, sleep,join的时 候受阻,调用了interrupt()方法,那么不仅会清除中断标志位,还会抛出InterruptedException异常)
isInterrupted():查询线程中断标志位,不会改变中断标志位
interrupted():第一次使用返回true,并清除中断标志位,在此之后查询中断状态isInterrupt()都会返回false,
(三)线程间的共享
1.synchronized内置锁
在并发编程中存在线程安全问题,主要原因有 :1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
Synchronized:是一种同步锁。作用是实现线程间同步,对同步的代码加锁,使得每一次,只能有一线程进入同步块,从而保证线程间的安全性,同时它还可以保证共享变量的内存可见性
它修饰的对象有以下几种:
a.修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的部分,进入同步代码前要获得给定对象的锁
b.修饰一个实例方法,进入同步代码前要获得当前实例的锁
c.修饰一个静态方法,进入同步代码前要获得当前类的锁
一句话总结synchronized:JVM会自动通过使用monitor来加锁和解锁,保证了同一时刻只有一个线程可以执行指定的代码,从而保证线程安全,同时具有可重入和不可中断的特性。
1、两个线程同时访问一个对象的相同的synchronized方法
同一实例拥有同一把锁,其他线程必然等待,顺序执行
2、两个线程同时访问两个对象的相同的synchronized方法
不同的实例拥有的锁是不同的,所以不影响,并行执行
3、两个线程同时访问两个对象的相同的static的synchronized方法
静态同步方法,是类锁,所有实例是同一把锁,其他线程必然等待,顺序执行
4、两个线程同时访问同一对象的synchronized方法与非synchronized方法
非synchronized方法不受影响,并行执行
5、两个线程访问同一对象的不同的synchronized方法
同一实例拥有同一把锁,所以顺序执行(说明:锁的是this对象==同一把锁)
6、两个线程同时访问同一对象的static的synchronized方法与非static的synchronized方法
static同步方法是类锁,非static是对象锁,原理上是不同的锁,所以不受影响,并行执行
7、方法抛出异常后,会释放锁吗
会自动释放锁,这里区别Lock,Lock需要显示的释放锁
3个核心思想:
1、一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待
2、每个实例都对应有自己的一把锁,不同的实例之间互不影响;例外:锁对象是*.class以及synchronized被static修饰的时候,所有对象共用同一把锁
3、无论是方法正常执行完毕还是方法抛出异常,都会释放锁
可参考:https://www.cnblogs.com/JsonShare/p/11433302.html
2.volatile关键字,最轻量级的同步锁
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
2.禁止进行指令重排序。(实现有序性)
3.volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
簡單來説,内存可见性,线程不安全(常见场景:一个线程写,多个线程读)
(詳情可參考https://blog.csdn.net/devotion987/article/details/68486942)
3.ThreadLocal
ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
从源码中可以推测出ThreadLocal本质上是一个Map,key为Thread,value为初始化值
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
下面我们用ThreadLocal写个demo
package com.thread.day1;
public class ThreadLocalTest extends Thread{
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1;
}
};
@Override
public void run() {
threadLocal.set(threadLocal.get()+1);
System.out.println("Thread name :["+Thread.currentThread().getName()+"]"+threadLocal.get());
}
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000);
for (int i = 0; i < 10; i++) {
new ThreadLocalTest().start();
}
}
執行結果
Thread name :[Thread-2]2
Thread name :[Thread-1]2
Thread name :[Thread-0]2
Thread name :[Thread-3]2
Thread name :[Thread-5]2
Thread name :[Thread-4]2
Thread name :[Thread-6]2
Thread name :[Thread-8]2
Thread name :[Thread-7]2
Thread name :[Thread-9]2
(四)线程之间的协作
1.等待和通知
wait()
notify()
notifyAll()
生产者与消费者
2.Join()
在某些情况下,子线程需要进行大量的耗时运算,主线程可能会在子线程执行结束之前结束,但是如果主线程又需要用到子线程的结果,换句话说,就是主线程需要在子线程执行之后再结束。这就需要用到join()方法
(简单来说就是插队)
3.Yield(),sleep(),wait(),notify(),notifyAll()含义
yield();一个线程调用yield()意味着告诉虚拟机自己非常乐于助人,可以把自己的位置让给其他线程(这只是暗示,并不表绝对)。但得注意,让出cpu并不代表当前线程不执行了。当前线程让出cpu后,还会进行cpu资源的争夺,但是能不能再次分配到,就不一定了
sleep():sleep()方法属于Thread类,让当前线程停止执行,把cpu让给其他线程执行,但不会释放对象锁和 监控的状态,到了指定时间后线程又会自动恢复可运行状态
wait():wait()属于Object类,与sleep()的区别是当前线程会释放锁,进入等待此对象的等待锁定池。比方说, 线程A调用Obj.wait(),线程A就会停止运行,而转为等待状态。至于等待多长时间? 那就看其他线程是否 调用Obj.notify().其优势显而易见,成为多个线程之间进行通讯的有手段!
注意:它必须包含在Synchronzied语句中,无论是wait()还是notify()都需要首先获得目标的对象的一个监视器