java多线程面试题这一篇就够了

基本概念

对操作系统来说,线程是最小的执行单元进程是最小的资源管理单元

一个进程至少包含一个主线程,也可以有更多的子线程。

线程拥有自己的栈空间。

同一时间,CPU只能处理1条线程,只有一条线程在工作(执行)

多线程并发(同时)执行,其实质是CPU快速的在多线程之间调度(切换)

线程不同状态之间的转化是谁来实现的呢

不是JVM,而是JVM通过操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。

如果线程过多,会怎样

CPU在N多条线程中调度,会消耗大量的cpu资源
每条线程被调度执行的频率越低(线程的执行效率低)

多线程的优点

能适当提高程序的执行效率
能适当提高资源的利用率(CPU 内存利用率等)

多线程的缺点

创建线程是有开销的
如果开启大量的线程,会降低程序的性能
程序设计更加复杂:线程之间的通讯,多线程的数据共享

主线程的主要作用

显示和刷新UI界面
处理UI事件(比如点击事件,滚动事件,拖拽事件等)

主线程的使用注意

别将比较耗时的操作放在主线程中,会导致UI界面的卡顿
将耗时操作放在子线程(后台线程,非主线程)

创建多线程的方式

  • 继承Thread类
  • 实现Runnable 接口
  • 实现Callable接口
  • 创建线程池 ExecutorService pool = Executors.newFixedThreadPool(5);

线程的生命周期

线程的生命周期

释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行
  • 当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束
  • 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁

不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
  • 应尽量避免使用 suspend() 和 resume() 来控制线程

在A线程中调用B线程的 join() 方法,表示:
当执行到此方法, A 线程停止执行,直至 B线程 执行完毕,A线程再接着 join() 之后的代码执行 ,join方法 的原理就是调用相应线程的 wait方法 进行等待操作的。

java中的线程状态转换

java中的线程状态转换

  1. 新建(New
    创建后尚未启动。
  2. 可运行(Runnable)
    可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。
  3. 阻塞(Blocking)
    等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
  4. 无限期等待(Waiting)
    等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法退出方法
没有设置 Timeout 参数的 Object.wait() 方法Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法被调用的线程执行完毕
LockSupport.park() 方法-
  1. 限期等待(Timed Waiting)
    无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
进入方法退出方法
Thread.sleep() 方法,使一个线程睡眠时间结束
设置了 Timeout 参数的 Object.wait() 方法,挂起一个线程时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法
LockSupport.parkUntil() 方法

阻塞 和 等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。
而 等待 是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

  1. 死亡(Terminated)
    可以是线程结束任务之后自己结束,或者产生了异常而结束。

在java中wait和sleep方法的不同

  • waitnotify方法定义在Object类中,因此会被所有的类所继承。 这些方法都是final的,即它们都是不能被重写。 而sleep方法是在Thread类中是由native修饰的,本地方法。
  • 在等待时wait会释放锁,而sleep一直持有锁。
  • 因为wait方法会释放锁,所以调用该方法时,当前的线程必须拥有当前对象的monitor,也即锁。要确保调用wait()方法的时候拥有锁,即wait()方法的调用必须放在synchronized方法或synchronized块中。
  • Wait通常被用于线程间交互,sleep通常被用于暂停执行。

start() 和run() 方法

线程调用start()方法时是由线程调度运行run()方法,直接调用run()方法,是不启动一个线程

多线程如何进行信息交互

java采用共享内存的并发模型
线程间通信使用:

  • Object中的waitnotifynotifyAll方法。
  • ReenterantLock得到的Condition中的awaitsignalsignalAll方法。

JMM

java memory model 与JVM的关系
Java内存模型(Java Memory Model,JMM)JMM主要是为了规定线程内存之间的一些关系。
根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

JMM

Java的并发采用的是共享内存模型,Java线程之间的通信总是显性进行。

主内存可以看成是堆,线程A在自己的本地内存中更改了共享变量的值,需要刷新到主内存中,才能同步到线程B。
A线程什么时候刷新主内存,B什么时候同步主内存都是不确定的。

jdk1.5 后 java.util.concurrent.atomic 类的小工具包提供原子变量类

  • AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference
  • AtomicIntegerArray 、AtomicLongArray
  • AtomicMarkableReference
  • AtomicReferenceArray
  • AtomicStampedReference

1.类中的变量都是volatile类型:保证内存可见性
2.使用CAS算法:保证数据的原子性(硬件级别的原子操作来实现CAS)

volatile关键字

Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程,可以保证内存中的数据可见

可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
对声明了volatile关键字的变量执行写操作的时候,JVM会向处理器发送一条Lock 前缀的指令,会把这个变量所在缓存行的数据写回到系统内存;

  • 对于多线程,不是一种互斥关系
  • 不能保证变量状态的“原子性操作”

volatile如何保证可见性和禁止指令重排序?

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile关键字的应用场景

参考

开销较低的“读-写锁”策略

使用锁进行所有变化的操作,使用 volatile 进行只读操作。
其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作。

如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    //读操作,没有synchronized,提高性能
    public int getValue() { 
        return value; 
    } 
 
    //写操作,必须synchronized。因为x++不是原子操作
    public synchronized int increment() {
        return value++;
    }

懒汉式单例

volatile 的一个语义是禁止指令重排序优化(即必须初始化好堆内存后,才将地址赋值给 instance 字段),也就保证了instance 变量被赋值的时候对象已经是初始化过的,从而避免了在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给 instance 字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用 getInstance,判断 instance!=null,返回,但是取到的其实是没有初始化的对象(实例变量还是默认值,而不是初始值),程序就会出错。

pubic class Singleton {
	private volatile static Singleton instace;   
	public static Singleton getInstance(){   
	    //第一次null检查     
	    if(instance == null){            
	        synchronized(Singleton.class) {      
	            //第二次null检查       
	            if(instance == null){         
	                instance = new Singleton();
	            }  
	        }           
	    }  
	    return instance; 
}

原子变量

1.类中的变量都是volatile类型:保证内存可见性
2.使用CAS算法:保证数据的原子性

sychronized关键字

在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

synchronized关键字最主要有以下3种应用方式,下面分别介绍:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

同步(Synchronization)基于进入退出管程(Monitor)对象实现

monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

monitor是由 ObjectMonitr 实现的,有两个队列,_WaitSet_EntryList,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后进入 _Owner 区域,若线程调用 锁对象的wait() 方法,将释放当前持有的monitorowner变量恢复为null,进入 WaitSet集合中等待被唤醒。其主要数据结构如下:

monitor

从字节码中可知同步代码块的实现使用的是monitorentermonitorexit 指令。

同步方法的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

notify/notifyAllwait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出 IllegalMonitorStateException 异常,这是因为调用这几个方法前必须拿到当前对象的监视器 monitor 对象,也就是说notify/notifyAllwait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

synchronized和Lock的区别

锁类型

  • 可重入锁:可重入锁就是当前持有该锁的线程能够多次获取该锁,无需等待。在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
  • 可中断锁:在等待获取锁过程中可中断
  • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
  • 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
类别synchronizedLock
存在层次Java的关键字,在 jvm 层面上是一个接口类,ReentrantLock 实现了 Lock 接口
锁的释放1、获取锁的线程执行完同步代码,自动释放锁 2、线程执行发生异常,jvm会让线程释放锁在 finally 中必须显示的释放锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有tryLock() 方法,可以尝试获得锁,如果锁被占用,返回false,否则返回true,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可)
性能少量同步大量同步

synchronized 是 JVM 提供的加锁,悲观锁;
lock是Java语言实现的,而且是乐观锁。
ReentrantLock是基于AQS实现的,由于AQS是基于FIFO队列的实现

可重入公平锁获取流程
在获取锁的时候,如果当前线程之前已经获取到了锁,就会把state加1,在释放锁的时候会先减1,这样就保证了同一个锁可以被同一个线程获取多次,而不会出现死锁的情况。这就是ReentrantLock的可重入性。

Lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。
ReadWriteLock接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

AQS

参考

AbstractQueuedSynchronizer 是java并发包中的一个抽象类,该类更像是一个框架,提供了一些模板方法供子类实现,从而实现了不同的同步器。

AQS内部维护了一个双向链表,head,tail分别指向头尾。

Node节点封装了尝试获取锁的线程对象。

volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。当state的值为0的时候,标识改Lock不被任何线程所占有。

AQS的很多操作都是基于CAS原子操作的,以确保线程安全。

Java线程池技术及原理

通过重用线程池中的线程,来减少每个线程创建和销毁的性能开销。可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

ThreadPoolExecutor 线程池的实现类
常见构造方法:ThreadPoolExecutor

 public ThreadPoolExecutor(int corePoolSize,
                               int maximumPoolSize,
                               long keepAliveTime,
                               TimeUnit unit,
                               BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
                               RejectedExecutionHandler handler) 

参数说明:

  • corePoolSize:核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
  • maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
  • keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收。
  • unit:指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
  • workQueue:线程池中的任务队列.常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。
  • threadFactory:线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法。通过线程工厂可以对线程的一些属性进行定制。
  • RejectedExecutionHandler:也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法。

线程池的线程执行规则跟任务队列有很大的关系:

  • 如果 线程数量<=核心线程数量,直接启动一个核心线程来执行任务,不会放入队列中
  • 如果线程数量 核心线程数<线程数量
    • 任务队列是LinkedBlockingQueue
      • 无大小限制:超过核心线程数量的任务会放在任务队列中排队。设置的最大线程数无效。线程数最多不会超过核心线程数。
      • 大小有限:如果队列没满,会加入队列,如果塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会调用handler。
    • 任务队列是SynchronousQueue(一种无缓冲的等待队列,缓存值为1的阻塞队列)
      • 线程池会创建新线程(非核心)执行任务,在任务完成后,闲置时间达到了超时时间就会被清除。当任务数量超过最大线程数时会直接调用handler。

ThreadPoolExecutor类中提供了几种饱和策略的写法:
new ThreadPoolExecutor.AbortPolicy()

  1. AbortPolicy:直接抛出异常(默认)
  2. CallerRunsPolicy:用调用者的线程执行任务
  3. DiscardOldestPolicy:丢弃队列里最久的一个任务,并执行当前任务。
  4. DiscardPolicy:抛弃当前任务

线程池不使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

线程池模式:先启动一定数目的工作线程。当没有请求工作的时候,所有的工人线程都会等待新的请求过来,一旦有工作到达,就马上从线程池中唤醒某个线程来执行任务,执行完毕后继续在线程池中等待任务池的工作请求的到达。
Client参与者,发送Request的参与者
Channel参与者,负责缓存Request的请求,初始化启动线程,分配工作线程。维护线程池和任务池
Worker参与者,具体执行Request的工作线程

工具类:Executors
方法有:
ExecutorService newFixedThreadPool(): 创建固定大小的线程池
ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需要自动的更改数量。
ExecutorService newSingleThreadExecutor():创建单个线程池。线程池中只有一个线程

java并发包concurrent及常用的类

  • 原子变量 :AtomicBoolean 、AtomicInteger等:类中的变量都是volatile类型:保证内存可见性 ;使用CAS算法:保证数据的原子性
  • java.util.concurrent 包还提供了设计用于多线程上下文中的Collection实现:
    当期望许多线程访问一个给定 collection 时,
    ConcurrentHashMap 通常优于同步的 HashMap,
    ConcurrentSkipListMap 通常优于同步的 TreeMap
    ConcurrentSkipListSet通常优于同步的 TreeSet.
  • CountDownLatch:闭锁,在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行
  • Java 5.0 在 java.util.concurrent 提供了一个新的创建执行线程的方式:Callable 接口
  • Lock 接口,并提供了与synchronized 相同的互斥性和内存可见性。但相较于synchronized 提供了更高的处理锁的灵活性。
  • ReadWriteLock 接口读写锁
  • Condition接口 控制线程通信,在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是await、signal 和 signalAll。
  • java.util.concurrent.Executor:负责线程的使用与调度的根接口。工具类:Executors

生产者/消费者模式

生产者/消费者模式

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumerTest {
	public static void main(String args[]) {
		final Queue<Integer> sharedQueue = new LinkedList();
		Thread producer = new Producer(sharedQueue);
		Thread consumer = new Consumer(sharedQueue);
		producer.start();
		consumer.start();
	}
}

class Producer extends Thread {
	private static final int MAX_QUEUE_SIZE = 5;
	private final Queue sharedQueue;

	public Producer(Queue sharedQueue) {
		super();
		this.sharedQueue = sharedQueue;
	}

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			synchronized (sharedQueue) {
				while (sharedQueue.size() >= MAX_QUEUE_SIZE) {
					System.out.println("队列满了,等待消费");
					try {
						sharedQueue.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				sharedQueue.add(i);
				System.out.println("进行生产 : " + i);
				sharedQueue.notify();
			}
		}
	}
}

class Consumer extends Thread {
	private final Queue sharedQueue;

	public Consumer(Queue sharedQueue) {
		super();
		this.sharedQueue = sharedQueue;
	}

	@Override
	public void run() {
		while (true) {
			synchronized (sharedQueue) {
				while (sharedQueue.size() == 0) {
					try {
						System.out.println("队列空了,等待生产");
						sharedQueue.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				int number = (int) sharedQueue.poll();
				System.out.println("进行消费 : " + number);
				sharedQueue.notify();
			}
		}
	}
}

协程

英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。Java的原生语法中并没有实现协程,Lua语言、Python、Go有协程。

哲学家进餐问题

https://blog.csdn.net/zxm1306192988/article/details/81199567

ThreadLocal

例子
ThreadLocal,线程本地变量,一般声明为static ,ThreadLocal为变量在每个线程中都创建了一个副本,不同线程只能从中 get,set,remove自己的变量,而不会影响其他线程的变量。

1、ThreadLocal.get(): 获取ThreadLocal中当前线程中保存的变量副本。
2、ThreadLocal.set(): 设置ThreadLocal中当前线程中变量副本。
3、ThreadLocal.remove(): 移除ThreadLocal中当前线程变量的副本。
4、ThreadLocal.initialValue(): protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,在线程第1次调用get()或set(Object)时才 执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

线程共享变量缓存如下:
Thread有个ThreadLocal.ThreadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。
这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。(ThreadLocalMap是ThreadLocal内部类)
ThreadLocalMap<ThreadLocal, Object>;
1、Thread: 当前线程,可以通过Thread.currentThread()获取。
2、ThreadLocal:我们的static ThreadLocal变量。
3、Object: 当前线程共享变量。
我们调用ThreadLocal.get()方法时,实际上是从当前线程中获取ThreadLocalMap<ThreadLocal, Object>,然后根据当前ThreadLocal获取当前线程共享变量Object。
ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。

这种存储结构的好处:
1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。
2、ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。

关于ThreadLocalMap<ThreadLocal, Object>弱引用问题:
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。
虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,我们有两种手段。
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

缺点:由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

如何控制线程执行顺序

启动一个线程到达就绪状态,不会立刻执行,需要等待CPU的调度,所以多个线程执行顺序不确定。

方式一:

通过 join 方法去保证多线程的顺序性执行
在这里插入图片描述
在A线程中调用B线程的join()方法,表示:当执行到此方法,A线程停止执行,直至B线程执行完毕,A线程再接着join()之后的代码执行 ,join方法的原理就是调用相应线程的wait方法进行等待操作的。
在这里插入图片描述

方式二:

使用 ExecutorService executorService=Executors.newSingleThreadExecutor();
创建只包含一个线程的线程池,他会维护一个FIFO队列,所有submit的任务按顺序执行。

在这里插入图片描述

Java如何实现线程安全

  • 互斥同步:使用 synchronized 关键字进行同步, 或使用 concurren t包中的 ReentrantLock 类.
  • 非阻塞同步:需要硬件指令完成. 如Compare-and-Swap (CAS),典型的应用在 AtomicInteger 中。
  • 无同步方案:将变量保存在本地线程中,就不会出现多个线程并发的错误了。java中主要使用的就是ThreadLocal这个类。

一般线程和守护线程的区别

java中的线程分为两种:守护线程(Daemon)和用户线程(User)。

通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页