1.线程的创建方式
继承Thread类
实现Runnable接口
实现Callable接口
使用线程池创建线程
--------------------------------------------------------------------------------
四种创建线程方法对比:
实现Runnable接口和实现Callable接口的方式基本相同,只不过Callable方式有方法的返回值,可以看成一种实现方式,与继承Thread相比:
多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
继承Thread类的实现方式不能继承其他的父类。
前三种方式如果创建关闭线程频繁,会消耗系统资源影响性能。在实际开发中主要使用线程池创建线程,因为线程池的线程可以回收利用,减少由线程创建和销毁所消耗的系统资源,可以提高性能。
创建线程本质只有1种,即创建Thread类,以上的所谓创建方式其实是实现run方法的方式的封装:
1、实现runnable接口的run方法,并把runnable实例作为target对象,传给thread类,最终调用target.run
2、继承Thread类,重写Thread的run方法,Thread.start会执行run方法
2.线程池的创建方式
1.使用Excetors中的静态方法进行创建六中不同的线程池
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】
2.创建ThreadPoolExcetor对象创建一个线程池
任务拒绝策略:
也可以自定义任务拒绝策略
3.对比
使用Executors的弊端是可能会创建大量的线程或者堆积大量的请求,导致oom,即java.lang.OutOfMemoryError,意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。所以更推荐ThreadPoolExcetor方式来创建线程池,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。
3.CAS
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。
CAS算法: 有三个操作数(内存值V, 旧的预期值 A, 要修改的值B)
当旧的预期值A= = 内存值 此时修改成功 将V改为B
当旧的预期值A != 内存值 此时修改失败 不做任何操作 并重新获取现在的最新值(自旋)
CAS(乐观锁)从乐观的角度出发 假设每次获取数据别人都不会修改 所以不会上锁 只在修改共享数据的时候会检查一下
如果修改过 重新获取数据
如果没有修改过 就直接修改
缺点:
1.循环时间长开销很大
自循环CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题。
2.只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
自适应自旋解决的是“锁竞争时间不确定”的问题,目标是降低线程切换的成本。
3.ABA问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
利用版本号机制解决ABA问题,一般是在数据表中加上一个数据库版本号version
字段,表述数据被修改的次数当数据被修改时version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
优点:
可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等。
CAS 是非阻塞的轻量级乐观锁,通过 CPU 指令实现。在资源竞争不激烈的情况下,synchronized 重量锁会进行比较复杂的加锁、解锁和唤醒操作,而 CAS 不会加锁,性能高。
4.synchronized和ReentrantLock的区别?
相似点:这两个同步方式有很多相似之处,他们都是加锁方式同步,而且都是阻塞式同步,也就是说当一个线程获取对象锁之后,进入同步块,其他访问该同步块的线程都必须阻塞在该同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态和内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
区别:这两种方式最大的区别就是对于synchronized来说,它是Java语言关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock他是jdk1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句来完成。相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下2项:
① 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
② Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。(公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。)
便利性:很明显Synchronized的使用方便简洁,并且由编译器去保证锁的加锁和释放锁,而ReentrantLock则需要手动声明加锁和释放锁的方法,为了避免忘记手动释放锁,最好是在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
性能区别:在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
5、讲一下synchronized关键字的底层原理?
说明:
① synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
② 在Java早期版本中,synchronized属于重量级锁,效率低下
-
因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。
-
如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
③ 在Java6之后Java 官方对从JVM层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。
5.1 特性
原子性
所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性
可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。
有序性
有序性值程序执行的顺序按照代码先后执行。
可重入性
synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
5.2 用法
synchronized可以修饰静态方法、成员函数,同时还可以直接定义代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是类。
5.3 实现
synchronized有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。他们的底层实现其实都一样,在进入同步代码之前先获取锁,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权 ,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来,一个是monitorenter和monitorexit指令操作,一个是通过方法flags标志。
① synchronized同步代码块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
从反编译的同步代码块可以看到同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
其实第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行goto
指令,而该指令转向的就是23行的return
,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。
② synchronized修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器减1,如果获取失败就阻塞住,直到该锁被释放。
6.有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程 //这里三个线程的启动顺序可以任意
t1.start();
t2.start();
t3.start();
}
}
7、如何停止一个正在运行的线程?
① 使用退出标志,使线程正常退出。
public class MyThread extends Thread {
volatile boolean flag = false ; // 线程执行的退出标记
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
t1.start();
// 主线程休眠6秒
Thread.sleep(6000);
// 更改标记为true
t1.flag = true ;
}
}
② 使用stop方法强行终止
stop()方法已经作废,因为如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,出现数据不一致的问题。
public class MyThread extends Thread {
volatile boolean flag = false ; // 线程执行的退出标记
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
t1.start();
// 主线程休眠2秒
Thread.sleep(6000);
// 调用stop方法
t1.stop();
}
}
③ 使用interrupt方法中断线程。
interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。
public class MyThread extends Thread {
volatile boolean flag = false ; // 线程执行的退出标记
public void run() {
while(!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
t1.start();
// 主线程休眠2秒
Thread.sleep(6000);
// 调用interrupt方法
t1.interrupt();
}
}
8.如果你提交任务时,核心线程池已经满了,这时会发生什么?
① 无界队列
如果使用的是无界队列LinkedBlockingQueue(底层是链表),没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务。
② 有界队列
如果使用的是有界队列比如ArrayBlockingQueue(底层是链表),任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
9.如何控制某个方法允许并发访问线程的数量?
在Java中,提供了对信号量Semaphore的支持。Semaphore类是一个计数信号量,必须由获取它的线程释放,通常可以用于限制并发访问的线程数目。
获得Semaphore对象:
public Semaphore(int permits, boolean fair)
permits:初始化可用的许可个数
fair:若该信号量保证在使用时按FIFO(先进先出)的顺序,则为true,否则为false;
两个重要的方法:
semaphore.acquire() 请求一个信号量(消费一个信号),这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量),如果信号被用完了则等待;
semaphore.release() 释放一个信号量,此时信号量个数+1。释放的信号新的线程就可以使用了.
public class CarThreadRunnable implements Runnable {
// 创建一个Semaphore对象,限制只允许2个线程获取到许可证
private Semaphore semaphore = new Semaphore(2) ;
@Override
public void run() { // 这个run只允许2个线程同时执行
try {
// 获取许可证
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "----->>正在经过十字路口");
// 模拟车辆经过十字路口所需要的时间
Random random = new Random();
int nextInt = random.nextInt(7);
TimeUnit.SECONDS.sleep(nextInt);
System.out.println(Thread.currentThread().getName() + "----->>驶出十字路口");
// 释放许可证
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类:
public class SemaphoreDemo01 {
public static void main(String[] args) {
// 创建线程任务类对象
CarThreadRunnable carThreadRunnable = new CarThreadRunnable() ;
// 创建5个线程对象,并启动。
for(int x = 0 ; x < 5 ; x++) {
new Thread(carThreadRunnable).start();
}
}
}
思考:
在很多情况下,可能有多个线程需要访问数目很少的资源。假设在服务器上运行着若干个回答客户端请求的线程。这些线程需要连接同一数据库,但任一时刻只能获得一定数目的数据库连接。要怎样才能够有效的将这些固定数目的数据库连接分配给大量的线程?
答:
1. 给方法加同步锁,保证同一时刻只能有一个线程去调用此方法,其他线程排队等待,但是此种情况下即使数据库连接有10个,也始终只有一个处于使用状态。这样会大大浪费系统资源,而且系统的运行效率极其低下。
2.使用信号量。通过信号量控制并发线程的数量 跟数据库连接个数相同,可以大大提高效率和性能。
10.Java中活锁和死锁有什么区别?
活锁:一个线程在执行的时候影响到了另外一个线程的执行,而另外一个线程的执行同时影响到了该线程的执行那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:甲向他自己的左边靠想让乙过去,而乙向他的右边靠想让甲过去。可见他们阻塞了对方。甲向他的右边靠,而乙向他的左边靠,他们还是阻塞了对方。
死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
11.如何进行死锁诊断?
1.使用jdk自带的工具: jstack
C:\Users\Administrator>jps
7408
8144 DeadThreadDemo1
3620
9108 Launcher
9180 Jps
C:\Users\Administrator>jstack -l 8144
2019-10-19 14:52:01
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11+28 mixed mode):
...
"Thread-0":
at com.itheima.javase.security.demo10.DeadThread.run(DeadThread.java:19)
- waiting to lock <0x000000008c591218> (a java.lang.Object) // 等待锁0x000000008c591218
- locked <0x000000008c591208> (a java.lang.Object) // 已经拥有的锁0x000000008c591208
"Thread-1":
at com.itheima.javase.security.demo10.DeadThread.run(DeadThread.java:29)
- waiting to lock <0x000000008c591208> (a java.lang.Object) // 等待锁0x000000008c591208
- locked <0x000000008c591218> (a java.lang.Object) // 已经拥有的锁0x000000008c591218
Found 1 deadlock. // 发现了一个1个死锁
C:\Users\Administrator>
2.使用jdk自带的图形化界面界面 jconsole 或 jvisualvm
12.Linux环境下如何查找哪个线程使用CPU最长?
步骤如下:
1、通过ps -ef | grep java查询指定的进程id
2、通过top -H -p pid查看指定进程中的线程信息
3、将线程的id的十进制数据转换成十六进制:printf "%x" tid
4、通过jstack -l pid查询进行中的线程nid(native thread id)