Java多线程

    科技2024-03-23  95

    线程

    启动方式

    1、继承Thread类,新建一个当前类对象,并且运行其start()方法

    public class test { public static void main(String[] args){ new MyThread1().start(); new MyThread1().start(); new MyThread1().start(); } } class MyThread1 extends Thread{ private int ticket=5; public void run() { while(ticket>0){ System.out.println("还剩下"+ticket+"张票"); ticket--; } } }

    2、实现Runnable接口,然后新建当前类对象,接着新建Thread对象时把当前类对象传进去,最后运行Thread对象的start()方法。 Thread类在调用start()函数后就是执行的是Runnable的run()函数。

    public class test { public static void main(String[] args){ MyThread2 myThread=new MyThread2(); new Thread( myThread ).start(); new Thread( myThread ).start(); } } class MyThread2 implements Runnable{ private int ticket=5; public void run() { while(ticket>0){ System.out.println("还剩下"+ticket--+"张票"); } } }

       1.在第二种方法(Runnable)中,ticket输出的顺序并不是54321,这是因为线程执行的时机难以预测,ticket–并不是原子操作。    2.在第一种方法中,我们new了3个Thread对象,即三个线程分别执行三个对象中的代码,因此便是三个线程去独立地完成卖票的任务;而在第二种方法中,我们同样也new了3个Thread对象,但只有一个Runnable对象,3个Thread对象共享这个Runnable对象中的代码,因此,便会出现3个线程共同完成卖票任务的结果。    3.第二种方法的线程不安全,同时操作一个变量的话,很有可能会出现不一样的输出结果,比如买票的顺序不一样,卖出两次5等等。        

    3、实现Callable接口,新建当前类对象,在新建FutureTask类对象时传入当前类对象,接着新建Thread类对象时传入FutureTask类对象,最后运行Thread对象的start()方法

    public class test { public static void main(String[] args){ MyThread3 myThread=new MyThread3(); FutureTask futureTask=new FutureTask( myThread ); new Thread( futureTask ).start(); new Thread( futureTask ).start(); try { System.out.println(myThread.call()); } catch (Exception e) { e.printStackTrace(); } } } class MyThread3 implements Callable{ private int ticket=5; public Object call() throws Exception { while(ticket>0){ System.out.println("还剩下"+ticket--+"张票"); } return ticket; } }

    Runnable 和 Callable ( Future Task)

    实现Runnable接口和实现Callable接口的区别:

    Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的Callable规定的方法是call(),该函数有返回值,Runnable规定的方法是run(),run()方法没有返回值Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void),call方法可以抛出异常,run方法不可以运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。 参考:https://blog.csdn.net/sinat_39634657/article/details/81456810

    多线程死锁

    两个线程都拿到对方需要的资源,互相不放锁,就会产生死锁

    public static void main(String[] args) { new Thread( () -> { System.out.println("线程一尝试锁住s1"); synchronized (s1) { System.out.println("线程一锁住s1"); try { System.out.println("线程一开始睡眠"); Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程尝试锁住s2"); synchronized (s2) { System.out.println("线程一锁住s2"); } } } ).start(); new Thread( () -> { System.out.println("线程二尝试锁住s2"); synchronized (s2) { System.out.println("线程二锁住s2"); try { System.out.println("线程二睡眠"); Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程二尝试锁住s1"); synchronized (s1) { System.out.println("线程二锁住s1"); } } } ).start(); }

    运行结果:

    如何 停止/中断 一个线程

    1.使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。

    public class ServerThread extends Thread { //volatile修饰符用来保证其它线程读取的总是该变量的最新的值 public volatile boolean exit = false; public void run() { ServerSocket serverSocket = new ServerSocket(8080); while(!exit){ serverSocket.accept(); //阻塞等待客户端消息 ... } } public static void main(String[] args) { ServerThread t = new ServerThread(); t.start(); ... t.exit = true; //修改标志位,退出线程 } }

    2.使用 stop() 方法强行终止线程(该方法已被弃用,直接停止线程会导致很多问题,比如连接未关闭,数据不同步,脏读幻读等等) 3.使用 interrupt 方法中断线程。interrupt不会立刻停止线程,准确来说是暂停线程,然后保证数据的安全退出,相比加stop()的直接停止来说,更加安全。

    public class InterruptThread1 extends Thread{ public static void main(String[] args) { try { InterruptThread1 t = new InterruptThread1(); t.start(); Thread.sleep(200); t.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } } public void run() { super.run(); for(int i = 0; i <= 200000; i++) { System.out.println("i=" + i); } }

    不同状态的转换

    1.Thread.sleep(1000)使线程休眠一秒   Thread.sleep(0)并不是没有作用的,作用是触发操作系统立刻重新进行一次CPU竞争。 2.join   等待相应线程运行结束。若线程A调用线程B的join方法,那么线程A将会暂停运行,直到线程B运行结束。(谁join进来就等谁先运行完)   join实际上是通过wait来实现的。 3.yield   使当前线程主动放弃对处理器的占用,这可能导致当前线程被暂停。   这个方法是不可靠的,该方法被调用时当前线程可能仍然继续运行(视系统当前的运行状况而定)。 4.wait和notify()/notifyAll()   需要注意的是   ①wait和notify/notifyAll方法只能在同步代码块里调用 wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在synchronized{}内部被调用的   ②notify操作必须要在wait操作之后,否则,可能导致线程不会醒来。   ③wait 5.LockSupport.park()/parkNano(time)/parkUntil(time)   LockSupport比Object的wait/notify有两大优势:   ①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。   ②unpark方法可以先于park方法调用,所以不需要担心线程间的执行的先后顺序

    挂起和等待有什么区别?

    线程挂起:   1.一般是被动的;   2.保存现场(寄存器,页面等等),部分内存交换出去,不访问,被动开启,好象中断一样. 线程等待:   1.一般是主动的   2.不保存现场信息,内存不变,等待-访问,等待开启(有可能是某个信号量),此时的线程处于Sleep状态(起码Windows是这样的).

    挂起和阻塞有什么区别?

    (1)挂起是一种主动行为,因此恢复也应该要主动完成。而阻塞是一种被动行为,是在等待事件或者资源任务的表现,你不知道它什么时候被阻塞,也不清楚它什么时候会恢复阻塞。 (2)阻塞(pend)就是任务释放CPU,其他任务可以运行,一般在等待某种资源或者信号量的时候出现。挂起(suspend)不释放CPU,如果任务优先级高,就永远轮不到其他任务运行。一般挂起用于程序调试中的条件中断,当出现某个条件的情况下挂起,然后进行单步调试。

    Java 中 Sleep 和 wait 的区别

    sleep()   1、属于Thread类,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态   2、sleep方法没有释放锁   3、sleep必须捕获异常   4、sleep可以在任何地方使用

    wait()   1、属于Object,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程   2、wait方法释放了锁   3、wait不需要捕获异常   4、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用

    Java 中线程调度算法

    线程调度有两种:抢占式和协同式   Java采取抢占式机制。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。   协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。   操作系统中可能会出现某条线程常常获取到VPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

    为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

           wait() 表示持有对象锁的线程A准备释放对象锁权限,释放cpu资源并进入等待。   notify() 表示持有对象锁的线程A准备释放对象锁权限,通知jvm唤醒某个竞争该对象锁的线程X。线程A synchronized 代码作用域结束后,线程X直接获得对象锁权限,其他竞争线程继续等待(即使线程X同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的notify ,notifyAll被调用)。   notifyAll() 表示持有对象锁的线程A准备释放对象锁权限,通知jvm唤醒所有竞争该对象锁的线程,线程A synchronized 代码作用域结束后,jvm通过算法将对象锁权限指派给某个线程X,所有被唤醒的线程不再等待。线程X synchronized 代码作用域结束后,之前所有被唤醒的线程都有可能获得该对象锁权限,这个由JVM算法决定。

           一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象

    线程中的数据共享

    方法一:多个线程公用一个Runable,这种方式只适用于多线程的操作相同

    public class sy1 { public static void main(String[] args){ MyThread myThread=new MyThread(); new Thread(myThread).start(); new Thread(myThread).start(); } } class MyThread implements Runnable{ private int ticket=5; public void run() { while(ticket>0){ System.out.println("卖出第"+ticket--+"张票"); } } }

    方法二:共享一个静态成员变量,可以进行不同的线程操作

    public class sy1 { private static int ticket=10; public static void main(String[] args){ MyThread myThread1=new MyThread(); MyThread2 myThread2=new MyThread2(); myThread1.start(); myThread2.start(); } static class MyThread extends Thread{ public void run() { while(ticket>0){ System.out.println("单人卖出第"+ticket--+"张票"); } } } static class MyThread2 extends Thread{ public void run() { while(ticket>0){ System.out.println("双人卖出第"+(ticket-2)+"和"+ticket--+"张票"); ticket--; } } } }

    方法三:JDK中解决线程共享数据(ThreadLocal)

    ThreadLocal

    作用:   ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量 应用场景:

    package org; public class sy2 { public static void main(String[] args){ PersonHeight personHeight=new PersonHeight(); new Thread(()->{ personHeight.setHight( 11 ); System.out.println(personHeight.getHight()); }).start(); new Thread(()->{ personHeight.setHight( 22 ); System.out.println(personHeight.getHight()); }).start(); } } class PersonHeight{ private int hight; public int getHight() { return hight; } public void setHight(int hight) { this.hight = hight; } }

    多线程操作一个变量,结果很有可能会出现异常,存在线程安全问题,这个时候就需要ThreadLocal

    public class sy2 { public static void main(String[] args){ ThreadLocal<PersonHeight> threadLocal=new ThreadLocal<>(); new Thread(()->{ threadLocal.set( new PersonHeight(11) ); System.out.println("线程一"+threadLocal.get().getHight()); }).start(); new Thread(()->{ threadLocal.set( new PersonHeight( 22 ) ); System.out.println("线程二"+threadLocal.get().getHight()); }).start(); } } class PersonHeight{ private int hight; public PersonHeight(int hight) { this.hight = hight; } public int getHight() { return hight; } public void setHight(int hight) { this.hight = hight; } }

    应用场景:

    变量在线程之间无需可见共享,为线程独有 变量创建和使用在不同的方法里且不想过度重复书写形参

    方法:

    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。

    实现方式   1.一个ThreadLocal对象内部有一个map,然后在每一个线程中获取值的时候,根据当前线程作为key到map中去获取对应的值 因为ThreadLocal中的值在每个线程中有一个副本,所以这种方式也非常的合适   但是缺点在于,多个线程会一起竞争同一个ThreadLocal对象的map,就会出现竞争,要想避免竞争,就要进行加锁   还有一个需要注意的点就是,如果我们的ThreadLocal的生命周期比Thread长,这种情况下要注意内存的泄漏问题

    public class CustomThreadLocal<T> { // 没有使用软引用,重点不在这里 private Map<Thread, T> map = new HashMap<>(); public T get() { Thread thread = Thread.currentThread(); synchronized (map) { return map.get(thread); } } public void set(T t) { Thread thread = Thread.currentThread(); synchronized (map) { map.put(thread, t); } } // 注意每次ThreadLocal使用结束之后一定要记得进行remove,否则会出现内存泄漏 public void remove(T t) { synchronized (map) { map.remove(t); } } public T initValue() { return null; } }

    2.一个ThreadLocal对象中没有任何的map,而是将Map放到了Thread对象中,每个线程获取值的时候,以ThreadLocal做为key到map中去获取   这种方式跟上面是反着过来的,这样有什么好处呢?每个线程访问ThreadLocal的时候,不会出现竞争问题,每个线程访问的是当前线程自己内部的map,避免了加锁

    public class CustomThread extends Thread { //注意,Thread类内部其实已经有了一个map,但是我们不能访问 //所以这里我们自己定义一个map来实现,主要是为了展示思想 private Map<CustomThreadLocal, Object> map = new HashMap<>(); public CustomThread(Runnable target) { super(target); } Object getValue(CustomThreadLocal local) { return map.get(local); } void set(CustomThreadLocal local, Object value) { map.put(local, value); } void remove(CustomThreadLocal local) { map.remove(local); } } public class CustomThreadLocal<T> { public T get() { Thread t = Thread.currentThread(); if (t instanceof CustomThread) { CustomThread thread = (CustomThread) t; T value = (T) thread.getValue(this); if (value == null) { value = initValue(); set(value); } return value; } return null; } public void set(T value) { Thread t = Thread.currentThread(); if (t instanceof CustomThread) { CustomThread thread = (CustomThread) t; thread.set(this, value); } } //remove方法非常的重要,在每次使用完Thread对象之后都应该去调用该方法 public void remove() { Thread t = Thread.currentThread(); if (t instanceof CustomThread) { CustomThread thread = (CustomThread) t; thread.remove(this); } } public T initValue() { return null; } }

    保证封闭 内存泄漏 OOM 问题

    守护线程

    线程分为用户线程和守护线程,用户线程就是我们平时创建使用的线程。如果程序中所有的用户线程都退出了,那么所有的守护线程就都会被杀死,很好理解,没有被守护的对象了,也不需要守护线程了。

    Join 加入线程

    多线程的优缺点

    线程池

           线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

    线程池的组成

    一个线程池包括四个基本部分   1.线程管理池(ThreadPool):用于创建并管理线程池,有创建,销毁,添加新任务;   2.工作线程(PoolWorker):线程池中的线程在没有任务的时候处于等待状态,可以循环的执行任务;   3.任务接口(Task):每个任务必须实现接口,用来提供工作线程调度任务的执行,规定了任务的入口以及执行结束的收尾工作和任务的执行状态等;   4.任务队列:用于存放没有处理的任务,提供一种缓存机制。

    线程池参数 ThreadPoolExecutor

    主要参数:   1.corePoolSize:核心线程数 核心线程会一直存活,及时没有任务需要执行。 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理。 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。   2.queueCapacity:任务队列容量(阻塞队列) 当核心线程数达到最大时,新任务会放在队列中排队等待执行。   3.maxPoolSize:最大线程数 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。   4.keepAliveTime:线程空闲时间 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。 如果allowCoreThreadTimeout=true,则会直到线程数量=0。   5.allowCoreThreadTimeout:允许核心线程超时   6.rejectedExecutionHandler:任务拒绝处理器 两种情况会拒绝处理任务:   (1)当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务。   (2)当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。 参数设置:

    https://baijiahao.baidu.com/s?id=1637828094805085849&wfr=spider&for=pc

    线程池的状态

    线程池的优缺点

    submit 和 execute 方法的区别

    execute方法开启线程执行池中的任务。submit是提交指定的任务去执行并且返回Future对象,即执行的结果 1.接受的参数不一样:   execute只能接受Runnable类型的任务   submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null 2.返回值不一样:   execute没有返回值   submit有返回值,所以需要返回值的时候必须使用submit

    线程池原理

    如果保证线程复用

    Synchronized

    Synchronized 的实现

    Synchronized 的可重入性?实现方式

    Monitor

    锁升级

    volatile

    功能 如何实现 原理

    Lock

    Lock 是什么,有那些实现类

    与 Synchronized 的区别, 如何选择

    AQS

    乐观锁和悲观锁的理解和实现,实现方式

    CAS

    ReentrantLock 实现公平锁和非公平锁

    Atomic

    原子类有哪些

    实现方法

    CAS

    并发容器

    ConcuurrentHashMap

    BlockingQueue 阻塞队列

    实现生产者-消费者模型

    常用多线程工具

    CountdownLatch

    CyliBarriar

    Semaphore

    Processed: 0.017, SQL: 8