单核CPU:所谓的多线程是假的!同一时间只会处理一段逻辑,只不过线程切换的快!像多个线程同时运行!
多核CPU:真正的多线程!多段逻辑同时工作!充分利用CPU!
单CPU时,线程间的上下文切换,反而会降低所谓多线程的效率!但是还是使用多线程,就是为了防止阻塞!(防止某一线程超时!)
这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。(大任务分解)
- 在单核CPU中,将CPU分成很小的时间片!在每个时刻只能有一个线程执行,是一种微观上轮流占用CPU的机制!
- 多线程会存在上下文切换!导致程序变慢;即采用一个拥有两个线程的进程执行所需的时间比一个线程的进程执行两次所需要的时间要多!
与进程相似,一个进程在执行过程中可以产生多个线程!
不同点:同类的多个线程共享同一块内存空间和一组系统资源;所以系统在产生线程和线程的切换时,负担比进程小。所以线程被称为轻量级进程!
是含有指令和数据的文件!被存储在磁盘或者其他数据存储设备当中,可以说是静态的代码!
与程序的关系:
是程序的一次执行过程,是系统运行程序的基本单位!动态的!
系统运行一个程序 就是 一个进程从 创建、运行、消亡的过程!(一个进程就是一个执行中的程序)
每个进程占有某些系统资源(CPU时间、内存空间、文件、输入输出设备的使用权等);当程序运行时,会被操作系统载入内存空间!与线程的关系:
线程是进程划分为更小的运行单位。
最大的不同:进程基本上是独立的,而线程则不一定,同一个进程的线程间可能会相互影响!
另一个不同:
进程属于操作系统范畴,同一时间段,可以执行一个以上的程序;
线程在同一程序内几乎同时执行一个以上的程序段!
七种方法:后面带了相应方法的例子
- 继承Thread类,作为线程对象存在
- 实现runnable接口,作为线程任务存在
- 匿名内部类创建线程对象
- 创建带返回值的线程
- 定时器Timer
- 线程池创建线程
- 利用java8新特性 stream 实现并发
public class CreatThreadDemo1 extends Thread{
/**
* 构造方法: 继承父类方法的Thread(String name);方法
* @param name
*/
public CreatThreadDemo1(String name){
super(name);
}
@Override
public void run() {
//interrupted方法,是来判断该线程是否被中断。
while (!interrupted()){
System.out.println(getName()+"线程执行了...");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CreatThreadDemo1 d1 = new CreatThreadDemo1("first");
CreatThreadDemo1 d2 = new CreatThreadDemo1("second");
d1.start();
d2.start();
//(终止线程不允许用stop方法,该方法不会施放占用的资源。)
d1.interrupt(); //中断第一个线程
}
}
- Thread.sleep(200); //线程休息2ms
- Object.wait(); //让线程进入等待,直到调用Object的 notify 或者 notifyAll 时,线程停止休眠
public class CreatThreadDemo2 implements Runnable {
@Override
public void run() {
while (true){
System.out.println("线程执行了...");
}
}
public static void main(String[] args) {
//将线程任务传给线程对象
Thread thread = new Thread(new CreatThreadDemo2());
//启动线程
thread.start();
}
}
Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。
public class CreatThreadDemo3 extends Thread{
public static void main(String[] args) {
//创建无参线程对象
new Thread(){
@Override
public void run() {
System.out.println("线程执行了...");
}
}.start();
//创建带线程任务的线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行了...");
}
}).start();
//创建带线程任务并且重写run方法的线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run 线程执行了...");
}
}){
@Override
public void run() {
System.out.println("override run 线程执行了...");
}
}.start();
}
}
创建带线程任务并且重写run方法的线程对象中:为什么只运行了Thread的run方法?
源码:
原因:Thread实现了Runnable接口,而Runnable接口里有一个run方法。
结果:匿名内部类创建线程对象的第三种,会调用Thread类的run方法。而不是Runnable接口的run方法。(输出:override run 线程执行了…)
public class CreatThreadDemo4 implements Callable {
public static void main(String[] args) throws ExecutionException,InterruptedException {
CreatThreadDemo4 demo4 = new CreatThreadDemo4();
FutureTask<Integer> task = new FutureTask<Integer>(demo4); //FutureTask最终实现的是runnable接口
Thread thread = new Thread(task);
thread.start();
System.out.println("我可以在这里做点别的业务逻辑...因为FutureTask是提前完成任
务");
//拿出线程执行的(call方法)返回值
Integer result = task.get();
System.out.println("线程中运算的结果为:"+result);
}
//重写Callable接口的call方法
@Override
public Object call() throws Exception {
int result = 1;
System.out.println("业务逻辑计算中...");
Thread.sleep(3000);
return result;
}
}
Callable接口介绍:
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
* 计算结果,或者抛异常(无法计算时)
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
返回指定泛型的call方法。然后调用FutureTask对象的get方法得到call方法的返回值。
public class CreatThreadDemo5 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器线程执行了...");
}
},0,1000); //延迟0,周期1s
}
}
public class CreatThreadDemo6 {
public static void main(String[] args) {
//创建一个具有10个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
long threadpoolUseTime = System.currentTimeMillis();
for (int i = 0;i<10;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了...");
}
});
}
long threadpoolUseTime1 = System.currentTimeMillis();
System.out.println("多线程用时"+(threadpoolUseTime1-threadpoolUseTime));
//销毁线程池
threadPool.shutdown();
threadpoolUseTime = System.currentTimeMillis();
}
}
操作系统中 有 三态模型 ->五态模型->七态模型;通常说的是五态模型。
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
3、使用interrupt方法中断线程。
1、start方法 用来启动相应的线程;
2、run方法 只是thread的一个普通方法,在主线程里执行;
3、需要并行处理的代码放在run方法中,start方法 启动线程后 自动调用 run方法;
4、run方法必须是public的访问权限,返回类型为void。
理解:
run() 是由JVM直接调用的,如果没有启动线程(调用start()方法),就直接调用run(),那么这个run方法其实是运行在当前线程中(如:main中),而不是之前定义的线程中!
只有调用了start()方法,才体现多线程的特性,不同线程里的run()中的代码交替执行!
如果只调用run(),那么代码还是同步代码,必须一个run结束才运行下一个run()。
1、
Runnable
接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
2、Callable
接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask
配合可以用来获取异步执行的结果。
利用这一点,可以解决这些问题:
某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?
Callable+Future/FutureTask
却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。
多线程访问同一段代码,不会产生不确定的结果!
对非安全的代码进行加锁控制
使用线程安全的类
多线程并发情况下,线程共享的变量改为方法级的局部变量
单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。(一段代码在分配到的时间片内没有完成,从运行->就绪;直到下一次调用,此时时间片分给另一段程序,发生上下文切换)
上下文切换时:要保存当前线程运行的位置到内存;并从内存中加载需要恢复的线程的信息!(耗时)
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
解决:
在临界区中使用适当的同步就可以避免竞态条件。
一种是用synchronized
,一种是用Lock显式锁
实现。
守护(Daemon)线程:运行在后台,为JVM中所有非守护(前台)线程的运行提供便利服务**(佣人)**:只要当前 JVM 实例中 尚存任何一个非守护线程没有结束,守护线程就全部工作;只有 最后一个非守护线程结束时,守护线程随着JVM一同结束工作!
用户(User)线程:运行在前台,执行具体的任务。如:main、连接网络的子线程等!
例如:main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
区别:用户线程结束,JVM退出(不管有没有守护线程在运行)。守护线程不影响JVM的退出!(因为没有被守护者,Daemon也就没有运行的必要了)
注意事项:
setDaemon(true)
必须在start()
方法前执行,否则会抛出IllegalThreadStateException
异常- 在守护线程中产生的新线程也是守护线程
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
- 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。
任何线程都可以被设置为守护线程和用户线程;
Thread.setDaemon(boolean);
//true则把该线程设置为守护线程,
//反之则为用户线程。
Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
守护线程相当于后台管理者 比如 : 进行内存回收,垃圾清理等工作
Java类库中大多数基本数值类如
Integer
、String
和BigInteger
都是不可变的。
如
Random
、ConcurrentHashMap
、Concurrent集合
、atomic
有条件线程安全的最常见的例子是遍历由
Hashtable
或者Vector
或者返回的迭代器
如
ArrayList
、HashMap
如System.setOut()、System.runFinalizersOnExit()
是一个操作系统服务,负责为Runnable 状态的线程分配CPU时间!一旦创建一个线程并启动它,他的执行便依赖于线程调度器的实现!
指将可用的CPU时间分配给可用的Runnable线程的过程!(可以基于线程优先级、线程等待时间)
线程调度并不受JVM控制,所以由应用程序来控制他是更好的选择!
一旦一个共享变量(类的成员变量、静态成员变量) 被 volatile
修饰后,有两层语义:
锁、(when)什么地方使用、修改可见性+原子性、是否优化
volatile
本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile
仅能使用在变量级别;synchronized
则可以使用在变量、方法、和类级别的。volatile
仅能实现变量的修改可见性,并不能保证原子性;synchronized
则可以保证变量的修改可volatile
不会造成线程的阻塞;synchronized
可能会造成线程的阻塞。volatile
标记的变量不会被编译器优化;synchronized
标记的变量可以被编译器优化。从实践角度而言,
volatile
的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic
包下的类,比如AtomicInteger
。
二者相似,但是功能不同!
Volatile变量
:可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。atomic
:AtomicInteger类提供的atomic方法可以让这种操作具有原子性参考16题的两层语义!volatile
不是原子性操作
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
1、2 在 3 之前;4、5 在 3 之后;
但是 1、2 与 4、5 的顺序由于不是volatile的,所以不确定!
由于
flag变量
为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
Volatile
一般用于 状态标记量 和 单例模式的双检锁Java Memory Model,JMM
定义了一种多线程访问Java内存的规范!
volatile变量
的使用规则!(屏障)happens-before
,即先行发生原则,定义了操作A 必然先行发生于 操作B 的一些规则!sleep
让CPU时不考虑优先级,直接让出!yield
只会给相同优先级或更高优先级的线程以运行的机会!sleep()
后进入阻塞(blocked)状态,而执行yield()
后进入就绪(ready)状态!sleep()
声明抛出InterruptedException,而yield()
没有声明异常!由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用
Thread.sleep(0)
手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
请记住:线程类的构造方法、静态块是被
new
这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
举例:假设Thread2中new了Thread1,main函数中new 了Thread2,那么:
1、Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
2、Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
Java中有两种异常
非运行时异常(Checked Exception)
:这种异常必须在方法声明的throws语句指定,或者在方法体内捕获。例如:IOException和ClassNotFoundException。运行时异常(Unchecked Exception)
:这种异常不必在方法声明中指定,也不需要在方法体中捕获。例如,NumberFormatException。因为run()不支持 throws语句,所以当 线程对象的 run 方法 抛出 非运行异常时,我们必须捕获并且处理他。
当线程抛出一个未捕获的异常时,JVM将有以下处理方法:
首先,查找线程对象的 未捕获异常处理器。
如果找不到,继续找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器。
如果还没找到,JVM 将继续查找默认的未捕获异常处理器。
如果没有一个处理器存在。JVM将执行默认行为:将堆栈异常记录打印到控制台,并退出程序!
UncaughtExceptionHandler接口
并且实现这个接口的uncaughtException()方法
package concurrency;
import java.lang.Thread.UncaughtExceptionHandler;
public class Main2 {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
class Task implements Runnable{
@Override
public void run() {
int numero = Integer.parseInt("TTT");
}
}
class ExceptionHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n", t.getId());
System.out.printf("Exception: %s: %s\n",
e.getClass().getName(),e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n",t.getState());
}
}
Thread类还有另一个方法可以处理未捕获到的异常,即静态方法
setDefaultUncaughtExceptionHandler()。
这个方法在应用程序中为所有的线程对象创建了一个异常处理器。
原则:同步的范围越小越好!
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
但是存在锁粗化现象!
例如:
说StringBuffer
,它是一个线程安全的类,自然最常用的append()
方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化
的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。(同一个操作一直加锁解锁,明显没必要,浪费性能!)
CAS,全称为Compare and Swap
,即比较-替换。
假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
现象:并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
解决:可以通过
AtomicStampedReference
「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。(给值加一个修改版本号)
自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的开销!
解决:CAS思想体现,有一个自旋次数,就是为了避开这个耗时问题……
问题:CAS只能对一个变量执行操作的原子性,如果对多个变量,CAS目前无法直接保证操作的原子性!
解决:(两种)
使用互斥锁来保证原子性;
将多个变量封装成对象,通过AtomicReference
来保证原子性!
AQS : AbstractQueuedSynchronizer
(抽象队列同步器)
CAS
是 java.util.concurrent 的基础;AQS
是java并发包的 核心。ReentrantLock、CountDownLatch、Semaphore 等等都用到了AQS;
以双向队列的形式连接所有的Entry,比方说
ReentrantLock
,所有等待的线程都被放在一个Entry中并联成双向队列。
AQS定义了对双向队列所有的操作,而只开放了
tryLock、tryRelease
方法 给开发者使用,开发者可以根据自己的实现重写 这两个方法,来实现自己的并发功能!
(可联系到如何用、好处、启动策略)
合理使用线程池带来的三个好处。
线程本地变量,指 ThreadLocal 中填充的变量属于 当前线程,该变量对其他线程是隔离的。ThreadLocal 为变量在每个线程中都创建副本,每个线程可以访问自己内部的副本变量。(常问)
//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离。
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
以空间换时间,每个Thread里面维护一个以开地址法实现的 ThreadLocal.ThreadLocalMap
,把数据进行隔离,数据不共享,排除线程安全问题。
内存结构图:
ThreadLocal.ThreadLocalMap
的 成员变量。部分源码:
public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap是Thread的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal中的关键方法set()和get():
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程t
ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
if (map != null)
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //创建一个新的ThreadLocalMap
}
public T get() {
Thread t = Thread.currentThread();//获取当前线程t
ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
if (map != null) {
//由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap的Entry数组:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
- Thread类持有一个类型为ThreadLocal.ThreadLocalMap 的 实例变量 ThreadLocals,即每个线程都有属于自己的ThreadLocalMap。
- ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 本身,value 是 ThreadLocal的泛型值。
- 每个线程在往ThreadLoacl 里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal 为引用,在自己的map里找对应的可以,从而实现线程隔离!
ThreadLocalMap 中使用的 key 是 ThreadLocal 的弱引用,容易在JVM内存空间不足时,被回收!
如果被回收,则,Entry 中的 key 不存在,而 value 还在,造成内存泄露的问题!
解决办法:
使用完ThreadLocal 后,及时的调用 remove()方法,释放内存空间!
数据库连接池
会话管理中使用!
这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁
区别在于:
wait()
方法立即释放对象监视
器,notify()/notifyAll()
方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
通常用来形容多线程间的相互影响。
比如一个线程占用了临界区资源,那么其他所有需要这个而资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执行。
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。
既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁。
虽然自旋锁效率比互斥锁高,但它会存在下面两个问题:
1、自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。
2、试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
要慎重使用自旋锁,适合 锁使用者 保持锁时间比较短 并且 锁竞争不激烈的情况。正是由于自旋锁使用者 一般保持锁时间非常短,所以采取自旋而不是睡眠(阻塞)。
newFixedThreadPool
创建一个指定工作线程数量的线程池。
每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始最大值,则将提交的任务存入到池队列中。
newCachedThreadPool
创建一个可缓存的线程池。
特点:
- 工作线程的创建数量几乎没有限制(小于Integer.Max_VALUE),这样可灵活的往线程池添加线程!
- 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认1分钟),则该工作线程自动终止。终止后,如果有新任务,则重新创建一个工作线程。
newSingleThreadExecutor
创建一个单线程化的 Executor,即只创建唯一的工作线程来执行任务,如果这个线程异常结束,会有另一个取代他,保证顺序执行!
- 特点:
可保证顺序执行,并在任意给定的时间不会有多个线程是活动的。
newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer
。
Executor
和 ExecutorService
这两个接口主要的区别是(四个):
ExecutorService
接口继承了 Executor
接口,是 Executor
的子接口Executor
接口定义了 execute()方法用来接收一个Runnable接口的对象,ExecutorService
接口中的 submit()方法可以接受Runnable和Callable接口的对象。Executor
中的 execute() 方法 不返回任何结果,ExecutorService
中的 submit()方法可以通过一个 Future 对象返回运算结果。ExecutorService
Executors
类提供工厂方法用来创建不同类型的线程池。
newSingleThreadExecutor()
创建一个只有一个线程的线程池,
newFixedThreadPool(int numOfThreads)
来创建固定线程数的线程池,
newCachedThreadPool()
可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。
Semaphore
有两个重要方法:
semaphore.acquire()
: 请求一个信号量,将信号量减1;(如果变为负,再次请求就会阻塞)
semaphore.release()
:释放一个信号量,此时信号量个数+1
虽然可以通过Executor 区创建,但是由于他的弊端,一般强制不使用它!
Executors 返回线程池对象的弊端如下:
FixedThreadPool
和SingleThreadExecutor
: 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。CachedThreadPool
和ScheduledThreadPool
: 允许创建的线程数量为Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
方式一:通过Executor框架的工具类Executors来实现
方式二:通过构造方法(ThreadPoolExecutor
)实现
线程池的线程数设置为CPU+1,减少线程上下文切换
时间集中在IO(IO密集型):因为IO不占用CPU,所以不要让所有CPU闲下来,可以加大线程池中的线程数目,让Cpu处理更多业务
时间集中在计算上(计算密集型):和上一种一样,线程数设置少一点,减少线程上下文切换;
这种更看重整体架构的设计,看能不能做缓存,能不能增加服务器,能不能使用中间件做任务拆分和解耦、最后根据上一种考虑线程池!
Thread.interrupt()
来中断一个线程就会设置中断标识为true。当中断线程调用静态方法
Thread.interrupted()
来检查中断状态时,中断状态会被清零。非静态方法
isInterrupted()
用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。
解决:多个线程之间访问资源的同步性!
作用:可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行!
早期效率低的原因:在Java早期版本中,synchronized 属于重量级锁,因为监视器锁(monitor)是依赖底层的操作系统的 MUtex Lock来实现的,Java的线程是映射到操作系统的原生线程上的。如果挂起、唤醒一个线程,都需要操作系统帮忙,此时线程间的上下文切换(用户态->内核态)耗时;
JDK1.6 优化:自旋锁、适应性自旋锁、锁消除、锁粗化、锁升级(无锁->偏向锁->轻量级锁->重量级锁)……(从JVM层面优化)
例:如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象。
原因
:因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized 关键字
加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
synchronized 关键字
加到实例方法上是给对象实例上锁。
尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
线程dump:线程堆栈;死循环、死锁、阻塞、页面打开慢等额问题
此外,Thread类提供了getStackTrace()
方法来获取线程堆栈!是一个实例方法,与具体实例线程绑定!
通过线程之间共享对象就可,然后通过wait/notify/notifyAll
、await/signal/signalAll
进行唤起和等待
阻塞队列(BlockingQueue) 就是为了线程之间共享数据设计的!
第一种:将共享数据封装到一个对象,把这个共享数据所在的对象传递给不同的Runnable(线程)。
第二种:将这些Runnable对象作为一个类的内部类,共享的数据作为外部类的成员变量,对共享数据的操作分配给外部类来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来操作外部类的方法,实现对数据的操作!
实例:
class ShareData {
private int x = 0;
public synchronized void addx(){
x++;
System.out.println("x++ : "+x);
}
public synchronized void subx(){
x--;
System.out.println("x-- : "+x);
}
}
public class ThreadsVisitData {
public static ShareData share = new ShareData();
public static void main(String[] args) {
//final ShareData share = new ShareData();
new Thread(new Runnable() {
public void run() {
for(int i = 0;i<100;i++){
share.addx();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
for(int i = 0;i<100;i++){
share.subx();
}
}
}).start();
}
}
多个线程抢夺有限个资源时,会出现死锁现象!
1.互斥条件:一段时间内,某一资源只能被一个线程占用,其他线程请求需要等待!
2. 请求和保持:一个线程去请求其他资源时,占用原有资源,不释放
3. 不可剥夺:线程已经占用的资源,除非自己使用完主动释放,不能被抢占!
4. 环路等待:a等B,B等C,C又等A
预防死锁
核心思想:分配资源之前,判断系统是否是安全的;若是,才分配。每分配一次资源就测试一次是否安全,不是资源全部就位后才测试。
具体不细说!
Java.util.concurrent.lock
中的 Lock 框架是锁定的一个抽象,允许把锁的实现作为Java类。具体的实现在特定的类中。(抽象类的作用)
ReentrantLock
类 实现了 Lock,与synchronized
相同的并发性和内存语义,添加了(锁投票、定时锁、可中断锁等候)等特性;
ReentrantLock
有一个与锁相关的获取计数器,如果拥有某个锁的线程再次得到锁,则计数器+1,释放时也需要释放两次才能真正释放!和synchronized 类似,只有退出最外层synchronized 块才释放锁
synchronized 关键字
底层原理属于 JVM 层面。
通过查看字节码,我们可以知道:
synchronized 同步语句块
的实现使用的是 monitorenter
和 monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取
monitor
(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized
修饰的方法 并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM 通过该ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
本质区别:synchronized
是关键字,ReentrantLock
是类!
类提供了更加灵活的特性
- 上锁
锁可用:直接CAS持有
不可用:判断是否可重入:
~ 可重入:锁状态+请求数
~ 不可重入:CAS循环进入等待队列的队尾!- 处理中断:
由LockSupport类
支持,通过JNI 代用本地操作系统 来完成挂起的任务。
被唤起后,先检查是否中断!
并发度就是 segment 数组(1.8以后是key.value数组)的大小(默认为16)
意味着:最多可同时16条线程操作!
是一个读写锁接口!读锁共享、写锁独占!
表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这
个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
都位于java.util.concurrent包下!
有任何问题,请联系我,一定及时修改