从本质上来说,线程是进程的实际执行单元,一个程序至少有一个进程,一个进程至少有一个线程,它们的区别主要体现在以下几个方面:
进程间是独立的,不能共享内存空间和上下文,而线程可以;
进程是程序的一次执行,线程是进程中执行的一段程序片段;
线程占用的资源比进程少。
程序的运行必须依靠进程,进程的实际执行单元就是线程。
多线程可以提高程序的执行性能。
使用 join() 方法,等待上一个线程的执行完之后,再执行当前线程。
使并行变成串行执行。
线程的创建,分为以下三种方式:
class ThreadDemo {
public static void main(String[] args) throws Exception {
MyThread thread = new MyThread();
thread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread........");
}
}
输出
Thread........
class ThreadDemo{
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable......");
}
}
输出
Runnable......
public class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable ..... ";
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
//用来定义线程返回结果
FutureTask<String> futureTask = new FutureTask<>(myThread);
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
try {
//获取结果
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
输出
Callable .....
Callable 的调用是可以有返回值的、Runanle接口是没有返回值的。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
Object object = new Object();
new Thread(()->{
synchronized (object){
System.out.println( LocalDateTime.now().toString());
try {
//等待2秒
object.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( LocalDateTime.now().toString());
}
}).start();
输出
2020-04-21T21:08:19.430
2020-04-21T21:08:21.432
先输出2020-04-21T21:08:19.430 然后2s之后 输出2020-04-21T21:08:21.432
当使用 wait() 方法时,必须先持有当前对象的锁,否则会抛出异常 java.lang.IllegalMonitorStateException。
使用 notify()/notifyAll() 方法唤醒线程。
注意:
wait() 、 notify()/notifyAll() 都是Object类的方法,sleep()是Thread类的方法
Object lock = new Object();
lock.wait();
lock.notify();
try {
System.out.println("执行前");
Thread.sleep(1000);
System.out.println("执行后");
} catch (InterruptedException e) {
e.printStackTrace();
}
输出
执行前
执行后
yield 方法是让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态。
使用 System.exit(0)
可以让整个程序退出;禁止在程序中使用
中断单个线程 interrupt()
对线程进行“中断”。
可以使用 isInterrupted() 判断线程是否被打断。
Thread t = new Thread() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
// 让同优先级的线程有执行的机会
if (this.isInterrupted()) {
break;
}
System.out.println(i);
}
}
};
t.start();
Thread.sleep(10);
t.interrupt();
}
我机器输出到753的时候线程停止了。
使用 setPriority 方法设置(1-10)优先级,默认的优先级是 5,数字越大表示优先级越高。
优先级高但不一定就表示执行优先,仅仅是发生的概率大而已。
Thread thread = new Thread(() ->{}); //jdk8 创建线程
t.setPriority(10);
t.interrupt();
线程的常用方法如下
死锁是指两个或者两个以上的线程互相持有对方的锁,互相等待对方释放锁,然后一直等下去这种情况叫做死锁。
实现代码
Object locak1 = new Object();
Object locak2 = new Object();
new Thread() {
@Override
public void run() {
synchronized (locak1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取locak2的锁 但是被第二个线程占用了
synchronized (locak2) {
}
}
}
}.start();
new Thread() {
@Override
public void run() {
synchronized (locak2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取locak1的锁 但是被第一个线程占用了
synchronized (locak1) {
}
}
}
}.start();
查看死锁
用JDk命令 先查看运行的类 和pid
然后利用 jstack pid 查看死锁
存在类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll() 直接唤醒。
守护线程是一种比较低级别的线程,一般用于为其他类别线程提供服务,因此当其他线程都退出时,它也就没有存在的必要了。例如,JVM(Java 虚拟机)中的垃圾回收线程。
在 JDK 8 中,线程的状态有以下六种。
submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。
ThreadPoolExecutor 最多包含以下七个参数:
shutdownNow() 和 shutdown() 都是用来终止线程池的
区别
当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。
Executors 可以创建以下六种线程池。
Executors 可以创建单线程线程池,创建分为两种方式:
newCachedThreadPool() 适合处理大量短时间工作任务。它会试图缓存线程并重用,如果没有缓存任务就会新创建任务,如果线程的限制时间超过六十秒,则会被移除线程池,因此它比较适合短时间内处理大量任务。
可执行周期性任务的线程池有两个,分别是:newScheduledThreadPool() 和 newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的单线程版本。
可执行周期性任务的线程池有两个,分别是:newScheduledThreadPool() 和 newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的单线程版本。
JDK 8 新增的线程池是 newWorkStealingPool(n),如果不指定并发数(也就是不指定 n),newWorkStealingPool() 会根据当前 CPU 处理器数量生成相应个数的线程池。它的特点是并行处理任务的,不能保证任务的执行顺序。
newFixedThreadPool 是 ThreadPoolExecutor 包装,newFixedThreadPool 底层也是通过 ThreadPoolExecutor 实现的。
单线程线程池提供了队列功能,如果有多个任务会排队执行,可以保证任务执行的顺序性。单线程线程池也可以重复利用已有线程,减低系统创建和销毁线程的性能开销。
使用 ThreadPoolExecutor 能让开发者更加明确线程池的运行规则,避免资源耗尽的风险。
Executors 返回线程池的缺点如下:
ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此 ThreadLocal 是线程安全的,每个线程都有属于自己的变量。
通过 ThreadLocal 的子类 InheritableThreadLocal 可以天然的支持多线程间的信息共享。
InheritableThreadLocal 存储数据使用的是是 InheritableThreadLocal
ThreadLocal 存储数据使用的是是 ThreadLocal
ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。
关键代码为 threadLocal.remove() ,使用完 ThreadLocal 之后,调用remove() 方法,清除掉 ThreadLocalMap 中的无用数据就可以避免内存溢出了。
答:ThreadLocal 和 Synchonized 都用于解决多线程并发,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别,
ReentrantLock 具备非阻塞方式获取锁的特性,使用 tryLock() 方法。ReentrantLock 可以中断获得的锁,使用 lockInterruptibly() 方法当获取锁之后,如果所在的线程被中断,则会抛出异常并释放当前获得的锁。ReentrantLock 可以在指定时间范围内获取锁,使用 tryLock(long timeout,TimeUnit unit) 方法。
答:new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)。
答:公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先 lock() 的线程不一定先获得锁。
答:lock() 和 lockInterruptibly() 的区别在于获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。
答:synchronized 和 ReentrantLock 都是保证线程安全的,它们的区别如下:
答:不对,tryLock(3, TimeUnit.SECONDS) 表示获取锁的最大等待时间为 3 秒,期间会一直尝试获取,而不是等待 3 秒之后再去获取锁。
答:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否尤其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。
… 待定 目前我不会
悲观锁和乐观锁并不是某个具体的“锁”而是一种并发编程的基本概念。乐观锁和悲观锁最早出现在数据库的设计当中,后来逐渐被 Java 的并发包所引入。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。
乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。
根据线程获取锁的抢占机制,锁又可以分为公平锁和非公平锁。
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
ReentrantLock 提供了公平锁和非公平锁的实现。
公平锁:new ReentrantLock(true)
非公平锁:new ReentrantLock(false)
如果构造函数不传任何参数的时候,默认提供的是非公平锁。
根据锁能否被多个线程持有,可以把锁分为独占锁和共享锁。
独占锁是指任何时候都只有一个线程能执行资源操作。
共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如 Java 中的 ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个线程进行写操作,允许多个线程读操作。
可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,其中 java.util.concurrent 包下的 AtomicInteger 就是借助 CAS 来实现的。
但 CAS 也不是没有任何副作用,比如著名的 ABA 问题就是 CAS 引起的。
synchronized 是悲观锁的实现,因为 synchronized 修饰的代码,每次执行时会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。
非公平锁
synchronized 使用的是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原由。
比如 A 占用锁的时候,B 请求获取锁,发现被 A 占用之后,堵塞等待被唤醒,这个时候 C 同时来获取 A 占用的锁,如果是公平锁 C 后来者发现不可用之后一定排在 B 之后等待被唤醒,而非公平锁则可以让 C 先用,在 B 被唤醒之前 C 已经使用完成,从而节省了 C 等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。
volatile 是 Java 虚拟机提供的最轻量级的同步机制。
当变量被定义成 volatile 之后,具备两种特性:
保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对于其他线程是可见的(可以立即得知的);
禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。
synchronized 既能保证可见性,又能保证原子性,而 volatile 只能保证可见性,无法保证原子性。比如,i++ 如果使用 synchronized 修饰是线程安全的,而 volatile 会有线程安全的问题。
CAS(Compare and Swap)比较并交换,CAS 是通过调用 JNI(Java Native Interface)的代码实现的,比如,在 Windows 系统 CAS 就是借助 C 语言来调用 CPU 底层指令实现的。
CAS 是标准的乐观锁的实现,会产生 ABA 的问题(详见正文)。
ABA 通常的解决办法是添加版本号,每次修改操作时版本号加一,这样数据对比的时候就不会出现 ABA 的问题了。