Java面试题,线程安全问题

news2024/9/24 21:20:38

线程安全问题

  • 一、对线程安全的理解(实际上是内存安全)
  • 二、Thread类的继承、Runable接口的重写
  • 三、守护线程
  • 四、ThreadLocal原理和使用场景
  • 五、sleep、wait、join、yield
  • 六、线程池、解释线程池参数

一、对线程安全的理解(实际上是内存安全)

  1. 堆是共享内存,可以被所有线程访问
  • 当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
  • 堆是进程共有的空间,也是线程的空间,分为全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄露。
  • 注意:局部堆和全局堆都是可以共享的
  1. 栈是线程安全的
  • 栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈相互独立。因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。
  1. 线程安全问题
    在每个进程的内存空间中都会有一块特殊的公共区域。通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成线程安全问题的潜在原因

二、Thread类的继承、Runable接口的重写

  1. Thread类实例(未同步)
class Store1{								//domain
	private int seq;
	public int get(){return seq;}
	public void put(int value){seq = value}
}

class Producer extends Thread{				//生产者Producer线程
	private Store1 store;
	private int num;
	public Producer(Store1 s,int num){		//构造方法
		store = s;
		this.num = num;
	}
	public void run(){						//线程体
		for(int i = 0; i<10; i++){
			store.put(i);
			System.out.println("Producer #" + this.num + "put:" + i);	//显示放数i
			try{
				sleep((int)(Math.random().100));
			}catch(InterruptedException e){}
		}
	}
}

class Consumer extends Thread{				//消费者Consumer线程
	private Store1 store;
	private int num;
	public Consumer(Store1 s,int num){		//构造方法
		store = s;
		this.num = num;
	}
	public void run(){						//线程体
		int value = 0;
		for(int i = 0; i<10; i++){
			value = store.get();
			System.out.println("Consumer #" + this.num + "got:" + value);	//从store对象取值
			try{
				sleep((int)(Math.random().100));
			}catch(InterruptedException e){}
		}
	}
}

public class MyProCon{
	public static void main(String[] args){
		Store1 s = new Store1();
		Producer p1 = new Producer(s,1);
		Consumer c1 = new Consumer(s,1);
		p1.start();
		c1.start();
	}
}
  1. synchronized关键字修饰方法
  • 标有synchronized的方法称为不同方法,当某线程访问某一资源时,被同步的方法不能同时使用这一资源。(实际上就是加了synchronized的方法,在执行时需要执行完整个方法)
  • 注意:这些方法能实现同步必须是同一实例对象的方法才能实现同步,因为一个对象一把锁,通过3.4.实例来感受一下
  1. Thread类(同步的典型错误案例)
public class VolatileThread extends Thread {  
  
    public synchronized void run() {  
        for (int i = 0; i < 10; i++) {  
            System.out.println(Thread.currentThread().getName());  
        }  
    }  
  
    public static void main(String[] args) {  
        for (int i = 0; i < 3; i++) {  
            VolatileThread vt = new VolatileThread();  
            //设置线程的名称,看在执行哪个对象的run()  
            vt.setName(i + "");  
            vt.start();  
        }  
    }  
} 

分析

在方法上加synchronized等同于synchronized(this),虽然看似给run()方法加上了锁,但是我们看main()中是如何去产生多个线程的,是分别new出了三个不同的线程对象。也就是说三个线程都拿到各自对象的锁,因此都能够执行run()中的代码。

  1. Thread类(实现同步)
class Store2{								//domain
	private int seq;
	private boolean available = false;			//Producer与Consumer共用
	public synchronized int get(){
		while(available == false){
			try{
				wait();
			}catch (InterruptedException e)
		}
		available = false;
		notify();
		return seq;
	}
	public synchronized void put(int value){
		while(available == true){
			try{
				wait();
			}catch (InterruptedException e)
		}
		seq = value;
		available = true;
		notify();
	}
}

class Producer extends Thread{				//生产者Producer线程
	private Store2 store;
	private int num;
	public Producer(Store1 s,int num){		//构造方法
		store = s;
		this.num = num;
	}
	public void run(){						//线程体
		for(int i = 0; i<10; i++){
			store.put(i);
			System.out.println("Producer #" + this.num + "put:" + i);	//显示放数i
			try{
				sleep((int)(Math.random().100));
			}catch(InterruptedException e){}
		}
	}
}

class Consumer extends Thread{				//消费者Consumer线程
	private Store2 store;
	private int num;
	public Consumer(Store1 s,int num){		//构造方法
		store = s;
		this.num = num;
	}
	public void run(){						//线程体
		int value = 0;
		for(int i = 0; i<10; i++){
			value = store.get();
			System.out.println("Consumer #" + this.num + "got:" + value);	//从store对象取值
			try{
				sleep((int)(Math.random().100));
			}catch(InterruptedException e){}
		}
	}
}

public class MyProCon{
	public static void main(String[] args){
		Store1 s = new Store1();
		Producer p1 = new Producer(s,1);
		Consumer c1 = new Consumer(s,1);
		p1.start();
		c1.start();
	}
}
  1. Runnable接口(实现同步)
public class RunnableThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + "----" +i);
        }
    }
}

public class RunnableThreadTest {
    public static void main(String[] args) {
        //1. 通过继承 Runnable 创建线程
        Runnable r = new RunnableThread();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r,"线程2");
        //开始执行
        t1.start();
        t2.start();
        
        //2. 使用内部类创建线程
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 100;i++){
                    System.out.println(Thread.currentThread().getName() + "----" +i);
                }
            }
        },"线程3");
        t3.start();
        
        //3. 使用匿名内部类创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 100;i++){
                    System.out.println(Thread.currentThread().getName() + "----" +i);
                }
            }
        },"线程4").start();
    }
}
  1. Thread与Runnable的相关问题
  • 两者使用场景:多线程编程主要就是为线程编写run()方法。如何选用这两种方法?其规则是:如果编写的类必须从其他类中导出,则选出第二种方法实现多线程。因为Java不支持多重继承,继承了其他类后不能再继承Thread类,只能利用Runnable接口。
  • 两者线程同步的区别:正如上述两个例子,Runnable可以实现多个相同的程序代码的线程去共享同一个资源,而Thread并不是不可以,而是相比于Runnable来说,不太合适
  • Runnable不可以直接run:多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可动行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。
  1. Thread实现Runnable源码
public class Thread implements Runnable {
	/** 这里只看一些 常见的参数 */
	/** 线程名 */
	private volatile char name[];
	/** 优先级 */
	private int priority;
	/** 是否为守护线程 */
	private boolean daemon;
	/** 线程要执行的目标任务 */
	private Runnable target;
	/** 所属线程组 */
	private ThreadGroup group;
	/** 类加载器 */
	private ClassLoader contextClassLoader;
	/** 
	 * ThreadLocal 能为线程设置线程私有变量 就是通过下面这个threadLocals变量完成的,
	 * ThreadLocal的get/set方法就是通过操作 各个线程的 threadLocals 变量实现的。
	 * 1、线程A持有一个 ThreadLocalMap 变量;
	 * 2、线程A调用一个类的 ThreadLocal变量 tlA 的 get/set方法;
	 * 3、tlA(ThreadLocal)的 get/set方法 获取当前线程A,调用 线程A 的 ThreadLocalMap变量 的get/put方法;
	 * 4、其它线程 调用 tlA(ThreadLocal)的 get/set方法 同理。
	 */
	ThreadLocal.ThreadLocalMap threadLocals;
	ThreadLocal.ThreadLocalMap inheritableThreadLocals;
	/** 线程栈的大小 */
	private long stackSize;
	/** 
	 * Thread类定义了6个线程状态:New、Runnable、Blocked、Waiting、TimedWaiting、Terminated(终止)
	 * 实际上还会把 Runnable 再细分为 就绪(未抢到时间片) 和 运行中(抢到时间片)
	 */
	private volatile int threadStatus;
	/** 最小优先级 */
	public static final int MIN_PRIORITY = 1;
	/** 中等优先级 */
	public static final int NORM_PRIORITY = 5;
	/** 最大优先级 */
	public static final int MAX_PRIORITY = 10;

	/**
	 * 内部枚举类,用来描述线程状态,状态值有: 
	 * NEW:          新建,还未调用start()方法;
	 * RUNNABLE:     运行,在java多线程模型中,就绪和运行都是运行状态; 
	 * BLOCKED:      阻塞; 
	 * WAITING:      等待,需要其他的线程来唤醒;
	 * TIMED_WAITING:超时等待,可以在指定的时间内自动醒来,如 sleep()方法;
	 * TERMINATED:   终止,线程执行完毕。
	 */
	public static final class State extends Enum {

		public static final State NEW;
		public static final State RUNNABLE;
		public static final State BLOCKED;
		public static final State WAITING;
		public static final State TIMED_WAITING;
		public static final State TERMINATED;
		private static final State VALUES[];

		static {
			NEW = new State("NEW", 0);
			RUNNABLE = new State("RUNNABLE", 1);
			BLOCKED = new State("BLOCKED", 2);
			WAITING = new State("WAITING", 3);
			TIMED_WAITING = new State("TIMED_WAITING", 4);
			TERMINATED = new State("TERMINATED", 5);
			VALUES = (new State[] { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED });
		}

		private State(String s, int i) {
			super(s, i);
		}
	}

	/**
	 * 一系列 构造方法 ------------------------------------------------------
	 * 可以看出来,其中都调用了init()方法,这也是一个约定俗成的规矩, 即,如果要在 new 时进行一些初始化操作,
	 * 那么请将初始化操作单独写在 init()方法中,然后在构造函数中调用该 init()方法
	 */
	public Thread() {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(null, null, (new StringBuilder()).append("Thread-").append(nextThreadNum()).toString(), 0L);
	}

	public Thread(Runnable runnable) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(null, runnable, (new StringBuilder()).append("Thread-").append(nextThreadNum()).toString(), 0L);
	}

	Thread(Runnable runnable, AccessControlContext accesscontrolcontext) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(null, runnable, (new StringBuilder()).append("Thread-").append(nextThreadNum()).toString(), 0L,
				accesscontrolcontext);
	}

	public Thread(ThreadGroup threadgroup, Runnable runnable) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(threadgroup, runnable, (new StringBuilder()).append("Thread-").append(nextThreadNum()).toString(), 0L);
	}

	public Thread(String s) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(null, null, s, 0L);
	}

	public Thread(ThreadGroup threadgroup, String s) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(threadgroup, null, s, 0L);
	}

	public Thread(Runnable runnable, String s) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(null, runnable, s, 0L);
	}

	public Thread(ThreadGroup threadgroup, Runnable runnable, String s) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(threadgroup, runnable, s, 0L);
	}

	public Thread(ThreadGroup threadgroup, Runnable runnable, String s, long l) {
		daemon = false;
		stillborn = false;
		threadLocals = null;
		inheritableThreadLocals = null;
		threadStatus = 0;
		blockerLock = new Object();
		init(threadgroup, runnable, s, l);
	}

	private void init(ThreadGroup threadgroup, Runnable runnable, String s, long l) {
		init(threadgroup, runnable, s, l, null);
	}

	/**
	 * 初始化线程
	 */
	private void init(ThreadGroup threadgroup, Runnable runnable, String name, long l,
			AccessControlContext accesscontrolcontext) {

		// 参数校验,线程name不能为null
		if (name == null)
			throw new NullPointerException("name cannot be null");
		this.name = name.toCharArray();
		// 当前线程就是该线程的父线程
		Thread parent = currentThread();
		SecurityManager securitymanager = System.getSecurityManager();
		if (threadgroup == null) {
			if (securitymanager != null)
				threadgroup = securitymanager.getThreadGroup();
			if (threadgroup == null)
				threadgroup = parent.getThreadGroup();
		}
		threadgroup.checkAccess();
		if (securitymanager != null && isCCLOverridden(getClass()))
			securitymanager.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
		threadgroup.addUnstarted();
		// 守护线程、优先级等设置为父线程的对应属性
		group = threadgroup;
		daemon = parent.isDaemon();
		priority = parent.getPriority();
		if (securitymanager == null || isCCLOverridden(parent.getClass()))
			contextClassLoader = parent.getContextClassLoader();
		else
			contextClassLoader = parent.contextClassLoader;
		inheritedAccessControlContext = accesscontrolcontext == null ? AccessController.getContext()
				: accesscontrolcontext;
		target = runnable;
		setPriority(priority);
		if (parent.inheritableThreadLocals != null)
			// 创建线程共享变量副本
			inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		stackSize = l;
		// 分配线程id
		tid = nextThreadID();
	}

	public synchronized void start() {
	    //假若当前线程初始化还未做好,不能start,0->NEW状态
	    if (threadStatus != 0)
	        throw new IllegalThreadStateException();

	    //通知group该线程即将启动,group的未启动线程数量减1
	    group.add(this);

	    boolean started = false;
	    try {
	    	// 调用native的start0()方法 启动线程,启动后执行run()方法
	        start0();
	        started = true;
	    } finally {
	        try {
	            //启动不成功,group设置当前线程启动失败
	            if (!started) {
	                group.threadStartFailed(this);
	            }
	        } catch (Throwable ignore) {

	        }
	    }
	}

	private native void start0();

	public void run() {
		if (target != null)
			target.run();
	}
	
	/**
	 * 请求终止线程。interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号,
	 * 告诉它要结束了,具体要中断还是继续运行,将由被通知的线程自己处理
	 */
	public void interrupt() {
	    if (this != Thread.currentThread())
	        checkAccess();
	    synchronized (blockerLock) {
	        Interruptible b = blocker;
	        if (b != null) {
	            interrupt0();
	            b.interrupt(this);
	            return;
	        }
	    }
	    interrupt0();
	}

	private native void interrupt0();

	/**
	 * 线程main 调用了线程A的join方法,则 线程main 会被阻塞,直到线程A执行完毕
	 */
    public final void join() throws InterruptedException {
        join(0);
    }

	/**
	 * 实际上是利用 wait/notify机制 来实现的
	 */
    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
		// millis 为 0,所以走这个分支
        if (millis == 0) {
        	// 当前线程是否还在运行,还在运行 则main线程 进入等待状态,直到 A线程运行完毕,将其唤醒
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

	/**
	 * 线程睡眠指定的时间,释放CPU资源,但不释放锁
	 */
	public static native void sleep(long millis) throws InterruptedException;

	/**
	 * 线程是否还在运行
	 */
	public final native boolean isAlive();
}

三、守护线程

  1. 守护线程的概念
  • 守护线程为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆
  • 守护线程类似于整个进程的一个默默无闻的小喽喽,它的生死无关重要,它却依赖整个进程而运行,哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断
  • 注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它,因为它不靠谱
  1. 守护线程的作用(举例)
  • GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终再低级别的状态中运行,用于实时监控和管理系统中的可回收资源
  1. 守护线程设置
  • 设置守护线程:thread.setDaemon(true)
  • 设置条件:thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个illegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
  • 注意:在Daemon线程中产生的新线程也是Daemon线程

四、ThreadLocal原理和使用场景

  1. ThreadLocal背景
  • 每个线程都会有属于自己的本地内存,在堆中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JMM管理控制写会到主内存中。
  • 很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
  1. ThreadLocal与Synchronized的区别
    在这里插入图片描述
  2. ThreadLocal的使用
  • 一般都会将ThreadLocal声明成一个静态字段
  • set(T value):设置线程本地变量的内容。
  • get():获取线程本地变量的内容。
  • remove():移除线程本地变量。
  • 注意:在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时,本地变量的旧状态仍然被保存。
public class Test {
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();   

    public void m1(User user) {threadLocal.set(user);}

    public void m2() {
        User user = threadLocal.get();		//获取user变量的副本
        /*
        	 使用
		*/
        threadLocal.remove();		 // 使用完清除
    }
}
  • 举例:未使用ThreadLocal和使用ThreadLocal
public class MyDemo01{
	private String content;
	private String getContent(){ return content;}
	private void setContent(String content){this.content = content;}

	public static void main(String[] args){
		MyDemo01 demo = new MyDemo01();
		for(int i = 0; i<5; i++){
			Thread thread = new Thread(new Runnable(){
				@Override
				public void run(){
					//每一个线程存一个变量,过一会儿再来取出这个变量
					demo.setContent(Thread.currentThread().getName() + "的数据");
					System.out.println("------------------");
					System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
				}
			});
		}
	}
}

在这里插入图片描述

public class MyDemo01{
	ThreadLocal<String> t1 = new ThreadLocal<>();
	
	private String content;
	private String getContent(){ 
		return t1.get();
	}
	private void setContent(String content){
		t1.set(content);
	}

	public static void main(String[] args){
		MyDemo01 demo = new MyDemo01();
		for(int i = 0; i<5; i++){
			Thread thread = new Thread(new Runnable(){
				@Override
				public void run(){
					//每一个线程存一个变量,过一会儿再来取出这个变量
					demo.setContent(Thread.currentThread().getName() + "的数据");
					System.out.println("------------------");
					System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
				}
			});
		}
	}
}

在这里插入图片描述

  1. 多个ThreadLocal怎么实现
  • 使用ThreadLocal作为key:由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子),所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。
public class Son implements Cloneable{
    public static void main(String[] args){
        Thread t = new Thread(new Runnable(){  
            public void run(){
            	ThreadLocal<Son> threadLocal1 = new ThreadLocal<>();
            	threadLocal1.set(new Son());
            	System.out.println(threadLocal1.get());
            	ThreadLocal<Son> threadLocal2 = new ThreadLocal<>();
            	threadLocal2.set(new Son());
            	System.out.println(threadLocal2.get());
            }}); 
        t.start();
    }
}
  1. Thread、ThreadLocal、ThreadLocalMap、Entry的原理
    在这里插入图片描述
  • 一个线程内部都有一个ThreadLocalMap
  • 一个ThreadLocalMap里面存储多个Entry
  • 一个Entry存储一个键值对,即线程本地对象ThreadLocal(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护,如下:
    在这里插入图片描述
  1. ThreadLocal为什么是弱引用
  • 首先了解什么是内存泄漏:不再会被使用的对象或者变量占用的呢村不能被回收,就是内存泄漏

下图:实现是强引用,虚线是弱引用
在这里插入图片描述

  • key使用强引用:当ThreadLocalMap的key使用强引用时,无法回收堆中的ThreadLocal。因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除ThreadLocalMap强引用的key,那么堆中的ThreadLocal不会被回收,导致Entry内存泄漏
  • key使用弱引用:当ThreadLocalMap的key使用强引用时,可以回收堆中的ThreadLocal。由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null时,在一下次ThreadLocalMap调用set()、get()、remove()方法的时候会被清楚value值的。

五、sleep、wait、join、yield

  1. 等待池和锁池
  • 等待池:当我们调用wait方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify或notifyAll后等待池的线程才会开始去竞争锁,notify是随机从等待池选出一个线程放到锁池,而notifyAll是将等待池的所有线程放到锁池当中
  • 锁池:所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待CPU资源分配
  1. sleep与wait
  • sleep方法不依赖同步器synchronized,但是wait需要依赖synchronized关键字
  • sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)
  • sleep一般用于当前线程休眠,或者轮询暂停操作,wait则多用于多线程之间的通信
  • sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。
    在这里插入图片描述
    在这里插入图片描述
  1. yield()和join()
  • yield()执行后线程直接进入就绪状态,马上释放了CPU的执行权,但是仍然参与CPU调度,所以有可能CPU下次进行线程调度还会让这个线程获取到执行权继续执行。
  • join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程
    在这里插入图片描述
public static void main(String[] args) throws InterruptedException{
	Thread t1 = new Thread(new Runnable(){
		@Override
		public void run(){
			try{
				Thread.sleep(3000);
			}catch(InterruptedException)
				e.printStackTrace();
			}
			System.out.println("222222222");
		}
	});
	t1.start();
	t1.join();
	//这行代码必须要等t1全部执行完毕,才会执行
	System.out.println("1111");
}

222222222
1111

六、线程池、解释线程池参数

  1. 为什么使用线程池
  • 降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。(因为线程的创建和消耗都是比较耗费资源的,所以使用线程池,将创建的线程保存在线程池中,需要使用的时候拿出来,不需要反复创建和销毁)
  • 提高响应速度:任务来了,直接从线程池中获取,而不是创建线程再执行
  • 提高线程可管理性:线程是稀缺资源(个数有限),使用线程池也可以统一分配调优监控
  1. 线程池的参数
  • corePoolSize:代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
  • workQueue:用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进行则全部放入队列,直到整个队列被放满但任务还再持续进入则开始创建新的线程。
  • maxinumPoolSize:代表最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,将workQueue都占满了,还无法满足需求时,此时就会创建新的线程,但是线程池内总数不会超过最大线程总数。
  • keepAliveTime、unit:表示超过核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超过核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime来设置空闲时间。
  • Handler:任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还没执行完正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。第二种情况就是当达到最大线程数,线程池已经没有能力继续处理提交的任务时,这时候也会拒绝
  • ThreadFactory:线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂

举个栗子:设corePoolSize=5,workQueue.size=5,maxinumPoolSize = 10

  • 现在来第1、2、3、4、5个任务,corePoolSize足够,所以可以直接创建线程。
  • 再来第6、7、8、9、10个任务,corePoolSize满了,所以后面来的任务都得在workQueue排队
  • 再来第11、12、13、14、15个任务,corePoolSize和workQueue都满了,所以需要创建Queue里面排队的线程(FIFO)
  • 后面再来任务,workQueue.size和maxinumPoolSize都满了,所以使用Handler拒绝

注意:最大线程数 = maxinumPoolSize.size;最大任务数 = maxinumPoolSize.size+ workQueue.size

  1. 线程池的处理流程(按照上面例子看就行了)
    在这里插入图片描述
  2. 线程池中的阻塞队列
  • 注意:阻塞队列与workQueue并不是同一个队列
  • 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务
  • 阻塞队列可以保证任务队列中没有任务时,阻塞常驻线程(corePoolSize创建的线程,它们会一直访问一个没有任务的workQueue,消耗CPU资源),使得线程进入wait状态,释放cpu资源
  • 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存储,不至于一直占用cpu资源
  1. 为什么是先添加队列而不是先创建最大线程?
  • 在创建新线程的时候,是要获取全局锁的,这个时候其他的线程就得阻塞,影响了整体效率。
  1. 线程池中的线程复用原理
  • 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应的一个任务的限制。所以我可以认为线程池的本质就是线程复用。
  • 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对于Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。
  • 核心:start()方法是创建新线程来执行run()任务,线程池就是不通过start()方法创建新线程,而是用旧现成的run()方法来执行任务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/176206.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM面试一

5. JVM 5.1 JVM包含哪几部分? 参考答案 JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区,内存分区),Execution Engine(执行引擎),Native Interface(本地库接口),下图可以大致描述 JVM 的结构。 JVM 是执行 Java 程序的虚拟计算…

【计算机组成原理】第一章 计算机系统概述

文章目录第一章 知识体系1.1 计算机发展历程1.1.1 计算机硬件的发展1.1.2 计算机软件的发展1.2 计算机系统层次结构1.2.1 计算机系统的组成1.2.2 计算机硬件1.2.3 计算机软件1.2.4 计算机的层次结构1.2.5 计算机系统的工作原理1.3 计算机的性能指标第一章 知识体系 1.1 计算机发…

35.Isaac教程--机械臂取放物体示例应用程序

机械臂取放物体示例应用程序 ISAAC教程合集地址文章目录机械臂取放物体示例应用程序使用 Omniverse 套件模拟驱动的机器人手臂启动取放示例应用程序该包为拾取和放置场景提供了一个应用程序脚手架。 它具有执行拾取和放置任务所需的高级步骤&#xff0c;并与两种类型的机器人操…

Java面试题,Spring与SpringBoot相关问题

Spring与SpringBoot相关问题1、BeanFactory和ApplicationContext有什么区别&#xff1f;2、描述一下Spring Bean的生命周期3、Spring的几种Bean的作用域4、单例Bean是线程安全的吗&#xff1f;5、Spring框架用到了哪些设计模式6、Spring事务的实现方式、隔离级别、传播行为7、S…

Lesson4--栈和队列

目录 1.栈 1.1栈的概念及结构 1.2栈的实现 初始化栈 销毁栈 栈的扩容 入栈 出栈 获取栈顶元素 获取栈中有效元素个数 判空 程序代码如下 Stack.h Stack.c test.c 2.队列 2.1队列的概念及结构 ​2.2队列的实现 初始化队列 队尾入队列 队头出队列 获取队列头部元素 获取…

二、pyhon基础语法篇(黑马程序猿-python学习记录)

黑马程序猿的python学习视频&#xff1a;https://www.bilibili.com/video/BV1qW4y1a7fU/ 目录 一 、print 1. end 2. \t对齐 二、字面量 1. 字面量的含义 2. 常见的字面量类型 3. 如何基于print语句完成各类字面量的输出 三、 注释的分类 1. 单行注释 2. 多行注释 3. 注释的…

多进程|基于非阻塞调用的轮询检测方案|进程等待|重新理解挂起|Linux OS

说在前面 今天给大家带来操作系统中进程等待的概念&#xff0c;我们学习的操作系统是Linux操作系统。 我们今天主要的目标就是认识wait和waitpid这两个系统调用。 前言 那么这里博主先安利一下一些干货满满的专栏啦&#xff01; 手撕数据结构https://blog.csdn.net/yu_cbl…

nacos源码分析==服务订阅-服务端推送被订阅者最新信息给订阅者

上一篇讲到客户端发送请求到服务端进行服务注册&#xff0c;注册后&#xff0c;服务端会发出两个事件&#xff0c;第一个事件会触发另一个ServiceChangedEvent&#xff0c;这个事件被com.alibaba.nacos.naming.push.v2.NamingSubscriberServiceV2Impl#onEvent 监听&#xff0c…

16. 条件控制

总体来说&#xff0c;条件控制的效果类似c/c/c#/java中的&#xff0c;只不过在语法格式的层面上存在一定的差异。 1. if条件语法格式 if condition_1:...elif condition_2:...else:...1、python 中用 elif 代替了 c/c中的 else if&#xff0c;所以if语句的关键字为&#xff1a…

高性能排序函数实现方案

如C语言的qsort()、Java的Collections.sort()&#xff0c;这些排序函数如何实现&#xff1f; 1 合适的排序算法&#xff1f; 线性排序算法的时间复杂度较低&#xff0c;适用场景特殊&#xff0c;通用排序函数不能选择。 小规模数据排序&#xff0c;可选时间复杂度O(n^2)算法大…

【算法】滑动窗口

目录1.概述2.算法框架3.应用本文参考&#xff1a; LABULADONG 的算法网站 1.概述 &#xff08;1&#xff09;滑动窗口可以用以解决数组/字符串的子元素相关问题&#xff0c;并且可以将嵌套的循环问题&#xff0c;转换为单循环问题&#xff0c;从而降低时间复杂度。故滑动窗口算…

【数据分析】(task5)数据建模及模型评估

note 文章目录note一、建立模型二、模型评估2.1 交叉验证2.2 混淆矩阵/recall/accuracy/F12.3 ROC曲线三、Pyspark进行基础模型预测时间安排Reference一、建立模型 下载sklearn的命令pip install scikit-learn。 from sklearn.model_selection import train_test_split impor…

ARP渗透与攻防(二)之断网攻击

ARP断网攻击 系列文章 ARP渗透与攻防(一)之ARP原理 1.环境准备 kali 作为ARP攻击机&#xff0c;IP地址&#xff1a;192.168.110.26 MAC地址&#xff1a;00:0c:29:fc:66:46 win10 作为被攻击方&#xff0c;IP地址&#xff1a;192.168.110.12 MAC地址&#xff1a;1c:69:7a:a…

Tkinter的Entry与Text

Tkinter界面设计之输入控件Entry以及文本框控件Text。 目录 一、放置控件 1. pack()函数 2. place()函数 3. grid()函数 二、简单控件 1. Entry输入控件 1.1 tk.StringVar()函数&#xff1a;接收一个字符串 1.2 tk.Entry()函数&#xff1a;设置一个输入控件E 2. Text文…

CMake多文件编译

之前学习ceres-solver中的3d相关的源码的时候&#xff0c;发现对于CMake多文件工程编译中对于CMakeLists.txt的编写和处理的理解运用还是比较模糊&#xff0c;这里整理梳理一下对于不同文件夹数量如何使用。 参考文章&#xff1a; CMake使用详解二&#xff08;多文件编译&…

maya常用操作

1&#xff1a;重置工作区。2&#xff1a;切换视图。按空格切换视图。3&#xff1a;未选中状态&#xff0c;按shift&#xff0c;再点右键&#xff0c;可以打开交互式创建。这样可以在栅格上创建想要的大小。不选中交互式创建的话&#xff0c;创建的是默认未知。默认未知为正中间…

linux系统中利用QT实现车牌识别的方法

大家好&#xff0c;今天主要和大家分享一下&#xff0c;如何利用QT实现车牌识别的方法。 目录 第一&#xff1a;车牌识别基本简介 第二&#xff1a;车牌识别产品申请 第三&#xff1a;百度车牌识别API接口 第四&#xff1a;车牌识别综合测试 第一&#xff1a;车牌识别基本简…

Scala快速入门

Scala简介 Scala是一门现代的多范式编程语言&#xff0c;平滑地集成了面向对象和函数式语言的特性。Scala运行于Java平台&#xff08;JVM&#xff0c;Java 虚拟机&#xff09;上&#xff0c;并兼容现有的Java程序&#xff0c;Scala代码可以调用Java方法&#xff0c;访问Java字…

ArcGIS Pro脚本工具(17)——生成多分式标注

​朋友们&#xff0c;你们知道ArcGIS里面分式标注的四种写法么&#xff1f; 放错图了&#xff0c;是这个 分式标注的四种形式我们可以把这类叫分式标注&#xff0c;网上也有博主分享过如何在ArcGIS中制作这类标注&#xff0c;但我觉得仍有一些不足。 一是基本都使用VB编写&…

中文问题相似度挑战赛

赛题概要 请本赛题排行榜前10位的队伍&#xff0c;通过作品说明提交源代码&#xff0c;模型以及说明文档&#xff0c;若文件过大&#xff0c;可发送至官网邮箱AICompetitioniflytek.com, 若截止时间内为提交&#xff0c;官方会通过电话联系相关选手&#xff0c;若未接到通知或…