Java多线程

本文详细介绍了Java多线程的基础概念,包括进程、线程、多线程的优势及注意事项。接着,深入探讨了Java中线程的启动、终止、yield()、join()和守护线程。此外,详细讲解了线程间的共享和协作,如synchronized、volatile和ThreadLocal,以及线程间的等待/通知机制。文章最后讨论了线程安全问题,强调了正确使用ThreadLocal以避免内存泄漏的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基础概念

  1. 进程:操作系统进行资源分配的最小单元,资源包含(CPU,内存,磁盘IO等)。比如:打开一个Office就是开启了一个进程
  2. 线程:CPU调度的最小单元,线程不能脱离进程存在。同一个进程中的多个线程共享进程的全部资源,每个线程仅仅只有很少部分私有资源(程序计数器,虚拟机栈,本地方法栈)。比如:打开多个word文档
  3. 多核心:一个芯片(CPU)上集成多个处理器核心,按照正常情况一个核心运行一个线程
  4. 多线程:让同一个处理器上的多个线程同步执行并共享处理器的执行资源,目前核心数:线程数 = 1:2
  5. 时间片轮转调度(RR算法):一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间
  6. 结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。进程进行上下文切换是一个很耗时的操作
  7. 并发量:单位时间内处理的量,不能脱离单位时间
    • 并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
    • 并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
      两者区别:一个是交替执行,一个是同时执行.

Java的多线程

多线程的好处
  1. 充分利用CPU的资源,减少CPU的空闲时间。1.6G的一个电脑,CPU执行一个指令的时间约为0.6纳秒(1秒= 1 0 9 {10^9} 109纳秒)
  2. 加快响应用户的时间,一个任务分成多个线程去执行,然后合并。减少整体的执行时间
  3. 可以使你的代码模块化,异步化,简单化。将相同和不同的模块进行拆分,如:下订单,发邮件,发短信等
多线程程序的注意事项
  1. 因为多个线程共享一个进程里面的所有资源,那么就可能带来线程安全问题,多个线程同时对一个共享全局变量、静态变量进行修改时(哪怕是最简单的 count++),也会造成数据的不准确
  2. 为了解决线程安全问题,加入了Java的锁机制,在同一个时刻保证只有一个线程访问共享变量,这样就不会有数据不准确问题,但是同时又会带来死锁问题。不同的线程都在等待那些根本不可能被释放的锁,如:两个线程相互持有对方需要的锁,又相互等待对方释放锁。
  3. 线程太多了会将服务器资源耗尽形成死机当机,每个线程运行都需要系统资源,系统内存以及CPU的“过渡切换”。解决这个问题,尽量用线程池,如果数据库连接池,资源池(资源库)等

Java天生就是多线程,就比如一个简单的main函数,JVM就会开启几个线程

public class OnlyMain {
    public static void main(String[] args) {
        //Java 虚拟机线程系统的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos =
                threadMXBean.dumpAllThreads(true, true);

        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            //1 纳秒 nanoseconds = 10的负9次方秒,计算CPU及各个硬件所运行的速度的运行单位。
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()+" times:"+threadMXBean.getThreadCpuTime(threadInfo.getThreadId()));
//            StackTraceElement[] stackTrace = threadInfo.getStackTrace();
//            for(StackTraceElement element : stackTrace) {
//                System.out.println("class:"+element.getClassName()+"-method:"+element.getMethodName());
//            }

        }
    }
}
运行结果:
cn.enjoyedu.ch1.base.OnlyMain
[6] Monitor Ctrl-Break times:31250000  //监控Ctrl-Break中断信号的
[5] Attach Listener times:0 //内存dump,线程dump,类信息统计,获取系统属性等
[4] Signal Dispatcher times:0  // 分发处理发送给JVM信号的线程
[3] Finalizer times:0 // 调用对象finalize方法的线程
[2] Reference Handler times:0  //清除Reference的线程
[1] main times:250000000  //main线程,用户程序入口
线程启动
  1. X extends Thread //继承Thread类
  2. X implements Runnable //实现Runnable接口
    X.start() //启动线程的唯一入口 --仅仅是使线程进入准备阶段,等待CPU分配时间片后,执行
    X.run() //仅仅是线程处理事件的方法,不是启动线程的方法,就相当于一个普通方法

注 意 : s t a r t ( ) 方 法 不 能 重 复 调 用 , 见 下 面 源 码 ; 而 r u n ( ) 可 以 重 复 调 用 \color{red}{注意:start()方法不能重复调用,见下面源码;而run()可以重复调用} start()run()

 public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
         //如果线程已经启动了,那么threadStatus = 1,再次调用start方法将会抛出 IllegalThreadStateException 异常
        if (threadStatus != 0) 
            throw new IllegalThreadStateException();
			……
			……
    }

Thread 是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象

线程终止
  1. run方法运行完,正常终止
  2. 调用stop() 方法强制终止线程,suspend()方法休眠线程,resume()方法,这些方法目前不建议使用,JDK已经标识为过期了,调用suspend()方法是线程不会释放已经占有的资源,带着资源进入休眠,容易造成死锁;同理,stop()方法在终结一个线程时强制线程立即终止,不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会
  3. 调用interrupt()方法,给线程打上中断标识,并通知线程应该中断了,但是具体线程是否终止还要取决于线程本身的处理。Java多线程之间是协作式,不是抢占式。线程通过检查自身的中断标志位是否被置为true来进行响应。
  • 线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
  • 如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、thread.yield等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false

注 意 : 处 于 死 锁 状 态 的 线 程 无 法 被 中 断 \color{red}{注意:处于死锁状态的线程无法被中断} 线

yield()方法

yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用yield方法。
所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

join()方法

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。(此处为常见面试考点)

Daemon(守护)线程

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
在这里插入图片描述

线程间的共享和协作

线程间的共享

synchronized内置锁
  1. 对象锁和类锁:
    对象锁是用于对象实例方法,或者一个对象实例上的
public class SyncDemo {
	private final static Object obj = new Object();
	//锁实例方法(实际上也是锁当前对象的一个实例(this))
	public synchronized void m1 {
		doSomething();
	}
	
	public synchronized void m2 {
		//锁对象实例
		synchronized (obj) {
			doSomething();
		}
		//锁this对象实例,同锁实例方法
		synchronized (this) {
			doSomething();
		}
	}
}

类锁是用于类的静态方法或者一个类的class对象上的

public class SyncDemo {
	private final static Object obj = new Object();
	//锁静态方法
	public synchronized static void m1 {
		doSomething();
	}
	
	public synchronized void m2 {
		//锁类的class对象
		synchronized (SyncDemo.class) {
			doSomething();
		}
		synchronized (任意类.class) {
			doSomething();
		}
	}
}
错误的加锁和原因分析

保证synchronized的对象是不会变的。

/**
 * 类说明:错误的加锁和原因分析
 */
public class TestIntegerSyn {

    public static void main(String[] args) throws InterruptedException {
        Worker worker=new Worker(1);
        //Thread.sleep(50);
        for(int i=0;i<5;i++) {
            new Thread(worker).start();
        }
    }

    private static class Worker implements Runnable{

        private Integer i;

        public Worker(Integer i) {
            this.i=i;
        }

        @Override
        public void run() {
        	//这里我们对i进行加锁
            synchronized (i) { 
                Thread thread=Thread.currentThread();
                //System.identityHashCode(i) 调用原始(最父级)的hashCode方法,可以理解为输出对象的内存地址
                System.out.println(thread.getName()+"--@"
                        +System.identityHashCode(i)); 
                //对加锁的对象进行了++操作,当进行++操作后,实际是new了一个新的Integer对象,所以每次锁的对象实际上不是同一个对象。所以这里锁不起作用
                i++;
                System.out.println(thread.getName()+"-------"+i+"-@"
                        +System.identityHashCode(i));
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName()+"-------"+i+"--@"
                        +System.identityHashCode(i));
            }
        }
    }
}
volatile,最轻量的同步机制

volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
volatile不能保证数据在多个线程下同时写时的线程安全,不能保证数据的原子性。
volatile最适用的场景:一个线程写,多个线程读。

ThreadLocal辨析

与synchonized的比较

ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的差别。
synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。
ThreadLocal是为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

案例:Spring的事务就借助了ThreadLocal类。Spring会从数据库连接池中获得一个connection,然会把connection放进ThreadLocal中,也就和线程绑定了,事务需要提交或者回滚,只要从ThreadLocal中拿到connection进行操作。

ThreadLocal的使用

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
void set(Object value)
设置当前线程的线程局部变量的值。
public Object get()
该方法返回当前线程所对应的线程局部变量。
public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
public final static ThreadLocal RESOURCE = new ThreadLocal();
RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论多少个线程并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

实现解析

在这里插入图片描述

 public T get() {
        Thread t = Thread.currentThread(); //得到当前线程
        ThreadLocalMap map = getMap(t); 
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
ThreadLocalMap getMap(Thread t) {
       return t.threadLocals;  //实际上返回的是当前线程,Thread类里面的一个变量
 }

//Thread类
public class Thread implements Runnable {
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

 

ThreadLocal的内部类ThreadLocalMap源码:

 static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
         //用了一个弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
			//类似于Map的Key,Value结构;Key就是当前ThreadLocal,Value就是当前要隔离访问的变量值
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        /**
        用数组保存Entry,因为一个线程可能有多个共享变量,需要隔离访问
        如:就有两个变量
        ThreadLocal<String> tl1 = new ThreadLocal<String>();
        ThreadLocal<Integer> tl1 = new ThreadLocal<Integer>();
        **/
        private Entry[] table; 

//得到Entry
private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

get方法,其实就是拿到每个线程独有的ThreadLocalMap(即:Thread类里面的 threadlocal变量)

引发的内存泄漏分析

在这里插入图片描述
当线程结束后,ThreadLocal将被GC回收,但是Value值这个时候还是强引用,GC回收不了
所以造成内存泄漏。防止内存泄漏,在用完ThreadLocal后及时调用remove()方法
实际上是调用ThreadLocal的expungeStaleEntry(),实际上set,get方法也会调用这个方法

private void remove(ThreadLocal<?> key) {
     Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);
      for (Entry e = tab[i];
           e != null;
           e = tab[i = nextIndex(i, len)]) {
          if (e.get() == key) {
              e.clear();
              expungeStaleEntry(i);
              return;
          }
      }
  }

  private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null; //将Value设置为null
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; //将Value设置为null
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
      

发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
总结
JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
JVM利用调用remove、get、set方法的时候,回收弱引用。
当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。

错误使用ThreadLocal导致线程不安全

如果ThreadLocal持有的是一个共享变量的引用,那么就会造成线程不安全,因为每个线程都可以修改这个对象里面的值。

/**
 * 类说明:ThreadLocal的线程不安全演示
 */
public class ThreadLocalUnsafe implements Runnable {
    //共享变量
    public static Number number = new Number(0);
    @Override
    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
        //将其存储到ThreadLocal中,存的是Number的对象的引用
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

   //ThreadLocal持有的是静态变量Nunber的一个引用
    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

  //五个线程都是持有同一个对象的引用,所以只要一个线程修改了对象里面的值,所有线程都会读到修改后的值
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }
}

线程间的协作

线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。却存在如下问题:
1)难以确保及时性。
2)难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

等待/通知机制

指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者 notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

  • notify(): 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
  • notifyAll(): 通知所有等待在该对象上的线程
  • wait():调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁
  • wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
  • wait (long,int):对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式

等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
在这里插入图片描述
通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
在这里插入图片描述
在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁, 执行notify()系列方法的线程退出调用了notifyAll的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值