一、概述
1. 基本概念
进程:进程是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,是竞争计算机系统资源(CPU、内存、IO等)的基本单位。
进程是系统中运行的一个程序,程序一旦运行就是进程,因此进程可以看成是程序执行的一个实例,比如:启动一个QQ.ext程序,在windows的任务管理器中就有一个进程。
进程中所包含的一个或多个执行单元称为线程。
线程:一个线程是进程的一个顺序执行流,是程序执行的最小单位,是比进程更小的独立运行的基本单位,线程也称为轻量级进程,是CPU调度和分配的基本单位,一个进程中可以包含多个线程。线程是一个进程里面不同的执行路径。比如:QQ能同时发送和接收消息,同时上传和下载文件等。
进程和线程的区别:每个进程拥有自己的一整套变量,而线程之间则共享数据。共享变量使线程之间的通信比进程之间的通信更有效、更容易。
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,
我们会用TPS或者QPS来反应这个系统的处理能力。
对于单核CPU来说,多个线程来回的切换执行,实际上在同一时间点只有一个线程真正的获得了CPU运行时间,那么此时的多线程还有高性能可言么?
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程, 我们只需要关注系统的内存,
cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
同步:这里的同步,可以理解为“协同步调”,Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。
我们的程序应该在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
线程的使用场合:
a. 线程通常用于在一个程序中需要同时完成多个任务的情况。我们可以将每个任务定义为一个线程,使他们得以一同工作;
b. 用于在单一线程中可以完成,但是使用多线程可以更快的情况。eg:下载文件。
线程的状态: 线程有以下六种状态
(1) New(新创建)
(2) Runnable(可运行):一旦调用start()方法,线程就处于runnable状态。一个正在运行中的线程仍然处于可运行状态。
(3) Blocked(被阻塞):当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。
当所有其他线程释放该所,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
(4) Waiting(等待)
(5) Timed waiting(计时等待)
(6) Terminated(被终止)
二、线程的使用
1. 创建线程
方式一:继承Thread并重写run方法
Thread类是线程类,它的每一个实例都表示一个可以并发运行的线程。我们可以通过继承该类并重写run方法来定义一个具体的线程。
重写run方法的目的只要是为了让线程执行我们所需要的逻辑。
class MyThread1 extends Thread{ public void run(){ for(int i=0;i<1000;i++){ System.out.println("你是谁啊?"); } } } Thread t1 = new MyThread1(); t1.start();
注意:
不要调用Thread或Runnable对象的run启动线程,如果直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程,即:主线程不会去创建一个新的线程去运行run方法里面的代码,而是在主线程 中调用Thread实现类的run方法(这相当多态),等这个run方法执行完以后,再接着执行后面的代码,这没有到达写线程的目的。
应该调用Thread.start()方法,这个方法将创建一个执行run方法的新线程。start()方法是由Thread类实现的,在这个方法里面有很多的实现。在调用start方法后,线程就处于可运行状态,这时主程序无序等待run方法执行完毕,而是继续执行下面的代码,只有等到新创建的这个线程获得了cup时间,才会开始执行run方法的线程体,这才是真正的实现了多线程。
线程启动后,何时运行,运行多久都听线程调度管理。线程对于线程调度的工作是不可控的,即:
a. 线程何时被分配CPU时间不可控,分配多久时间不可控。线程不能主动向线程调度要时间只能被动被分配;
b. 线程调度会尽可能将CPU时间均匀的分配给所有线程,但不保证一个线程一次这样规律的切换;
此种创建线程的方式有两个不足:
a. 由于需要继承Thread,而java又是单继承原则,这就导致当前类不能在继承其他类,很多时候会在实践开发中出现继承冲突问题。
b. 由于在线程内部重写run方法定义了当前线程要执行的任务,这就导致了线程与任务有一个强耦合关系,不利于线程重用。
方式二:单独定义线程任务,实现Runnable接口
实现Runnable接口并重写run方法来定义线程体,然后在创建线程的时候将Runnable的实例传入并启动线程,或者通过线程池来执行。
public class ThreadDemo2 { public static void main(String[] args) { Runnable r1=new MyRunnable1(); Runnable r2=new MyRunnable2(); Thread t1=new Thread(r1); Thread t2=new Thread(r2); t1.start(); t2.start(); } } class MyRunnable1 implements Runnable{ public void run() { for(int i=0;i<1000;i++){ System.out.println("你最喜欢的人是谁?"); } } } class MyRunnable2 implements Runnable{ public void run(){ for(int i=0;i<1000;i++){ System.out.println("我最喜欢的人是xxx!"); } } }
优点:
a. 可以将线程与线程中我们所要执行的业务逻辑分离开来,减少耦合;
b. 解决了java中单继承的问题,因为接口是多继承的;
使用匿名内部类完成以上两种线程的创建方式:
使用该方式可以简化编写代码的复杂度,当一个线程仅需要一个实例时,我们可以通过此方式创建。
//方式一 new Thread(){ public void run(){ for(int i=0;i<1000;i++){ System.out.println("你爱我么?"); } } }.start(); //方式二 new Thread(new Runnable(){ public void run(){ for(int i=0;i<1000;i++){ System.out.println("我爱你"); } } }).start();
方式三:实现Callable接口
Callable的 call() 方法提供返回值用来表示任务的运行结果,同时还可以抛出异常。
Callable创建的任务只能通过线程池执行,即通过线程池的 submit() 方法提交,submit()方法返回Future对象,通过Future对象的get()
方法可以获得具体的计算结果。而且get是个阻塞的方法,如果当前的这个任务未执行完,则一直等待当前的任务执行完毕,再返回这个任务的结果。同时它还提供了可以检查计算是否完成的方法。
对于Calleble来说,Future和FutureTask均可以用来获取任务执行结果,不过Future是个接口,FutureTask是Future的具体实现,
而且FutureTask还间接实现了Runnable接口,也就是说FutureTask可以作为Runnable任务提交给线程池。
package thread; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; /** * Callable接口的使用 * @author zls * @date 2020/3/31 */ public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1.创建一个线程池,同时创建3个线程在这个线程池内 ExecutorService pool = Executors.newFixedThreadPool(3); // 创建5个有返回值的任务(同时可以处理多个任务,多余的任务会排队,当处理完一个马上就会去接着处理排队中的任务。) List<Future> list = new ArrayList<Future>(); for (int i = 0; i < 5; i++) { // 2.创建任务对象 Callable c = new MyCallable(i + " "); // 3.线程执行任务并获取Future对象(从线程池中取出线程运行任务) Future f = pool.submit(c); list.add(f); } // 4.关闭线程池 pool.shutdown(); // 5.获取所有并发任务的运行结果 for (Future f : list) { // 从Future对象上获取任务的返回值,并输出到控制台 System.out.println("获取线程执行结果>>>:" + f.get().toString()); } } } /** * 创建线程的第三种方式,实现Callable接口 */ class MyCallable implements Callable<Object> { private String taskNum; MyCallable(String taskNum) { this.taskNum = taskNum; } /** * 带有返回结果的任务 */ @Override public Object call() throws Exception { Thread.sleep(500); long id = Thread.currentThread().getId(); return "当前线程id为:"+ id+", 任务为: "+taskNum; } }
此外,对于Callable创建线程,还可以通过不通过线程池来创建,参考:java创建线程的多种方式
补充:
Lamdba表达式的强大之处就是可以传递代码,而Runnable和Callable接口都是符合Lambda要求的函数式接口。因此,可以不用实现这两个接口,而是直接将接口的实现代码传递给Thread的target即可。
2. 线程常用api
2.1 线程优先级
线程优先级有10个等级,分别用数字1-10表示其中1最低,10最高,5为默认值。 理论上,优先级越高的线程,获取CPU时间片的次数多。
Thread max=new Thread(){ public void run(){ for(int i=0;i<10000;i++){ System.out.println("max"); } } }; Thread min=new Thread(){ public void run(){ for(int i=0;i<10000;i++){ System.out.println("min"); } } }; Thread norm=new Thread(){ public void run(){ for(int i=0;i<10000;i++){ System.out.println("norm"); } } }; max.setPriority(Thread.MAX_PRIORITY); min.setPriority(Thread.MIN_PRIORITY); min.start(); norm.start(); max.start();
2.2 守护线程
守护线程(daemon thread),是个服务线程,准确地来说就是服务其他的线程。
守护线程拥有结束自己声明周期的特征,而非守护线程不具备这个特点。
如果JVM中没有一个正在运行的非守护线程时,JVM会退出。JVM中的垃圾回收线程就是典型的守护线程,正因为如此,当垃圾回收线程还在运行着的时候,如果JVM退出了,垃圾回收线程(守护线程)可以自动的结束自己的生命周期。
使用场景:
通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
package thread; import lombok.SneakyThrows; /** * 当一个进程中的所有前台线程(非守护线程)都结束后,进程结束, 这时进程中的所有正在运行的后台线程(守护线程)都会被强制中断。 * * 解释: rose为非守护线程,jack为守护线程,当rose执行结束以后,jack自动结束。如果jack先执行完(结束),则rose会等到自己执行结束才结束。 * * @author zls * @date 2020/3/30 */ public class DaemonTest { public static void main(String[] args) { // rose:前台线程 Thread rose = new Thread() { @SneakyThrows public void run() { for (int i = 0; i < 3; i++) { System.out.println("rose: 我喜欢你!"); Thread.sleep(500); } System.out.println("rose: 你不喜欢我,我就....."); } }; // jack:后台线程 - 守护线程 Thread jack = new Thread() { @SneakyThrows public void run() { while (true) { System.out.println("jack: 我不喜欢你!"); Thread.sleep(500); } } }; //设置为守护线程,必须在线程启动前设置 jack.setDaemon(true); rose.start(); jack.start(); } }
2.3 yield()
该方法用于使当前线程主动让出当次CPU时间片回到Runnable状态,等待分配时间片。
2.4 join()
该方法是用来协调线程间的同步的,当一个线程调用另一个线程的join方法时,该线程会进入阻塞状态,直到另一个线程工作完毕才会解除阻塞。(相当于方法的调用执行过程)
package thread; import lombok.SneakyThrows; /** * 线程中join方法的使用 * 背景:以下案例模拟图片的下载加载过程 * 首先,图片要下载完 * 其次,再加载图片 * * @author zls * @date 2020/3/30 */ public class JoinTest { public static void main(String[] args) { // 1.下载图片的线程 final Thread download = new Thread() { @SneakyThrows public void run() { System.out.println("down:开始下载图片..."); for (int i = 1; i <= 100; i++) { i += 20; System.out.println("down:" + i + "%"); Thread.sleep(50); } System.out.println("down:图片下载完毕!"); } }; // 2.加载图片的线程 Thread show = new Thread() { @SneakyThrows public void run() { System.out.println("show:开始加载图片.."); // 在加载图片前应当等待下载线程先将图片下载完毕 // 一个线程A调用另个线程的B的join方法时,该线程A会进入阻塞状态,只有等到线程B执行完了以后才会接触阻塞。 download.join(); System.out.println("show:显示图片完毕!"); } }; download.start(); show.start(); } }
2.5 线程阻塞sleep()
static void sleep(long ms): 该方法会将调用该方法的线程阻塞指定毫秒,当阻塞超时后, 线程会自动回到runnable状态等待分配CPU 时间片继续并发运行。
该方法声明抛出一个InterruptException(如果有线程中断了当前线程),所以在使用该方法时需要捕获这个异常。该方法的可读性较差,因此
java.util.concurren包提供了一个可读性更好的线程暂停操作类TimeUnit,通常用来替换Thread.sleep()。
2.6 wait()
该方法使当前线程进入休眠状态,并将其置入等待队列中,直到在其他线程调用此对象的notify()方法或notifyAll()方法将其唤醒。
在调用wait()之前,线程必须要获得该对象的对象级别锁,因此只能在同步方法或同步块中调用wait()方法,进入wait()方法后,当前线程释放锁。
wait()和sleep()的区别:
a. sleep()是线程中的方法,但是wait()是Object中的方法;
为啥wait()方法不放在Thread类中?因为Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。
b. sleep()方法不会释放锁,但是wait()会释放锁,而且会加入到等待队列中;
验证代码:
package thread; import lombok.SneakyThrows; import java.util.stream.Stream; /** * @author zls * @date 2020/4/5 */ public class WaitTest { private final static Object lock = new Object(); public static void main(String[] args) { /** * 1.测试sleep: * 解释:线程1和线程2谁先抢到CPU资源,谁就先 执行 - 睡眠2s - 执行,在睡眠的过程中另个线程是无法执行的, * 直到执行完以后,另个线程才开始执行,这说明sleep方法不会释放锁。 * */ Stream.of("线程1", "线程2").forEach((n) -> new Thread(n){ @Override public void run() { // WaitTest.testSleep(); } }.start()); /** * 2.测试wait:作为对比试验,注释上面代码后在测试 * 解释:线程3先抢到CPU资源,先执行,然后休眠2s,在休眠的过程中,线程4是同样可以进入同步代码块执行, * 这说明wait释放了锁 * */ Stream.of("线程3", "线程4").forEach((n) -> new Thread(n){ @Override public void run() { WaitTest.testWait(); } }.start()); } @SneakyThrows private static void testSleep() { synchronized (lock) { System.out.println(Thread.currentThread().getName()+":正在执行"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName()+":休眠结束"); } } @SneakyThrows private static void testWait() { synchronized (lock) { System.out.println(Thread.currentThread().getName()+":正在执行"); lock.wait(2000); System.out.println(Thread.currentThread().getName()+":休眠结束"); } } }
c. sleep()方法不依赖于同步器synchronized,但是wait()需要依赖synchronizd关键字;
wait()/notify()方法定义在Object类中。如果线程要调用对象的wait()方法,必须首先获得该对象的监视器锁,调用wait()之后,当前线程又立即释放掉锁,线程随后进入WAIT_SET(等待池)中。如果线程要调用对象的notify()/notifyAll()方法,也必须先获得对象的监视器锁,调用方法之后,立即释放掉锁,然后处于Wait_set的线程被转移到Entry_set(等锁池)中,去竞争锁资源。The Winner Thread,也就是成功获得了对象的锁的线程,就是对象锁的拥有者,会进入runnable状态。由于需要获得锁之后才能够调用wait()/notify()方法,因此必须将它们放到同步代码块中。参考:[wait为什么要放到同步代码块中]
d. sleep()不需要被唤醒,但是wait()需要(不指定时间需要被别人唤醒)。
验证代码:
package thread; import lombok.SneakyThrows; /** * wait/notify方法使用测试 * 解释:如果没有唤醒方法,那第一个线程就会处于一直等待的状态,第二个线程唤醒了之后就不再等待了。 * @author zls * @date 2020/4/5 */ public class WaitTest1 { private final static Object lock = new Object(); public static void main(String[] args) { // 这个线程一直在等待 new Thread(WaitTest1::testWait).start(); // 这个线程去唤醒 new Thread(WaitTest1::testNotifyWait).start(); } @SneakyThrows private static void testWait() { synchronized (lock) { System.out.println("wait一直在等待"); lock.wait(); System.out.println("wait被唤醒了"); } } @SneakyThrows private static void testNotifyWait() { synchronized (lock) { Thread.sleep(5000); System.out.println("睡眠5s后唤醒wait..."); lock.notify(); } } }
2.7 notify()
唤醒等待的线程,如果监视器种只有一个等待线程,使用notify()可以唤醒。但是,如果有多条线程notify()是随机唤醒其中一条线程,
与之对应的就是notifyAll()就是唤醒所有等待的线程。
2.5 suspend()/resume()
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。
典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
wait()/notify()和suspend()/resume()之间的区别?
区别的核心在于: 前面叙述的所有方法方法,阻塞时都不会释放占用的锁(如果占用了的话),而wait/notify方法则相反。 上述的核心区别导致了一系列的细节上的区别: 首先,前面叙述的所有方法都隶属于Thread 类,但是wait/notify却直接隶属于 Object类,也就是说,所有对象都拥有wait/notify方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的wait()方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的 notify() 方法则导致从调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。 其次,前面叙述的所有方法都可在任何位置调用,但是wait/notify方法却必须在 synchronized方法或块中调用,理由也很简单,只有在synchronized方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用 wait/notify 方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。 关于 wait() 和 notify() 方法最后再说明两点: 第一:调用 notify()方法导致解除阻塞的线程是从因调用该对象的 wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。 第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll()方法将把因调用该对象的 wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。 谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend()方法和不指定超时期限的 wait()方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。 以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。
这确实很绕,如果你看了以后依然不理解,可参照: wait、notify为什么要放在同步代码块中
Java多线程运行环境中,在哪些情况下会使对象锁释放?
答:由于等待一个锁的线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不再需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁: (1)执行完同步代码块,就会释放锁。(synchronized) (2)在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。(exception) (3)在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进入对象的等待池。(wait) 除了以上情况以外,只要持有锁的线程还没有执行完同步代码块,就不会释放锁。 在下面情况下,线程是不会释放锁的: (1)执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,进入堵塞状态,在睡眠中不会释放锁。 (2)在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,回到就绪状态,但不会释放锁。
2.8 获取线程相关信息的一系列方法
//获取运行当前代码片段的线程 Thread t=Thread.currentThread(); //返回该线程的标识符 long id=t.getId(); //返回该线程的名称 String name=t.getName(); //返回该线程的优先级 int priority=t.getPriority(); //测试线程是否处于活动状态 boolean isAlive=t.isAlive(); //测试线程是否为守护线程 boolean isDaemon=t.isDaemon(); //测试线程是否已经中断 boolean isInterrupted=t.isInterrupted();
三、线程同步
多线程并发安全问题:
当多个线程访问同一资源时,由于线程切换时机不确定,可能导致多个线程执行代码出现混乱,导致程序执行出现问题,严重时可能导致系统瘫痪。
解决并发安全问题,就是要将多个线程"抢"改为"排队"执行(将异步的操作变为同步操作)。
异步操作:多线程并发的操作,相当于各干各的;
同步操作:有先后顺序的操作,相当于你干完我在干(协同步调)。
1. synchronized
synchronized关键字是java的同步锁。
同步代码块包含两部分:
a. 锁对象的引用;
b. 由这个锁保护的代码块。
语法:
synchronized(同步监视器---锁对象的引用){
// 代码块
}
执行机制:
每个java对象都可以用作一个实现同步的锁,线程进入同步代码块之前会自动获取锁,在退出同步代码块时候会自动释放锁(正常退出和抛出异常都是一样的);要想获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或者方法。
1.1 同步方法
当一个方法被Synchronized修饰后,那么该方法称为"同步方法",即多个线程不能同时进入方法内部执行。
方法上使用synchronized,那么锁对象就是当前方法所属对象,即: this.
package thread; /** * 当一个方法被Synchronized修饰后,那么该方法称为"同步方法",即多个线程不能同时进入方法内部执行。 * 方法上使用synchronized,那么锁对象就是当前方法所属对象,即:this * 场景: * 假设车站里面有20张票,两个线程相当于两个售票窗口,不断的售票。 * 说明: * 为了对比,可以把同步方法的关键字去掉,发现运行时,居然可以越过临界条件 * * @author zls * @date 2020/4/2 */ public class SynchronizedMethodTest { public static void main(String[] args) { final Station station = new Station(); // 窗口一: Thread t1 = new Thread(() -> { while (true){ int ticket = station.getTicket(); //模拟CPU执行到这里没有时间 Thread.yield(); System.out.println("窗口1售出一张票,还剩:" + ticket); } }); // 窗口二: Thread t2 = new Thread(() -> { while (true){ int ticket = station.getTicket(); //模拟CPU执行到这里没有时间 Thread.yield(); System.out.println("窗口2售出一张票,还剩:" + ticket); } }); t1.start(); t2.start(); } } /** * 车站里面有20张票 */ class Station { private int tickets = 5; /* * 同步方法 */ public synchronized int getTicket() { if (tickets == 0) { throw new RuntimeException("票已售完..."); } //模拟CPU执行到这里没有时间 Thread.yield(); return --tickets; } }
1.2 同步代码块
在使用同步块时,应当在允许的情况下尽量减少同步范围,以提高并发的执行效率。
使用同步块要注意同步监视器对象(上锁对象)的选取:
a. 通常使用this即可;
b. 若多个线程调用同一个对象的某些方法时,也可以将这个对象上锁;
多个需要同步的线程在访问该同步块时,看到的应该是同一个对象引用,否则达不到同步的效果;
比如,在一个方法中多个线程调用同一个集合的方法: list.add(xxx), 这时可以将这个集合list作为上锁的对象。
原则: 多个线程看到的上锁对象是同一个时,才有同步效果。
package thread; /** * 同步代码块 * * 使用同步块要注意同步监视器对象(上锁对象)的选取: * a. 通常使用this即可; * b. 若多个线程调用同一个对象的某些方法时,也可以将这个对象上锁; * 比如,在一个方法中多个线程调用同一个集合的方法: list.add(xxx), 这时可以将这个集合list作为上锁的对象。 * 原则: 多个线程看到的上锁对象是同一个时,才有同步效果。 * * 场景: * A、B两人同时去一家店里买衣服,她们可以同时挑选衣服,但是试衣间只有一个,必须等到一个试完另一个才能去试。 * @author zls * @date 2020/4/2 */ public class SyncCodeTest { public static void main(String[] args) { final Shop shop = new Shop(); Thread t1 = new Thread(() -> { shop.buy(); }); Thread t2 = new Thread(() -> { shop.buy(); }); t1.start(); t2.start(); } } class Shop{ public void buy(){ Thread t = Thread.currentThread(); try { System.out.println(t.getName()+":正在挑选衣服..."); Thread.sleep(5000); // 同步代码块 synchronized (this) { System.out.println(t.getName()+":正在试衣服..."); Thread.sleep(5000); } System.out.println(t.getName()+":结账离开"); } catch (Exception e) { e.printStackTrace(); } } }
1.3 同步静态方法
静态方法被synchronized修饰后,该方法是一个同步静态方法,任何时候都有同步效果。
静态方法上锁的对象是当前类的类对象(Class类型实例)。
每个类在JVM加载时,JVM都会实例化一个Class类型的实例用于描述加载的这个类,而在JVM内部,每个类都有且只有一个Class类型的实例与之对应,静态方法锁的就是这个对象。
package thread; import lombok.SneakyThrows; /** * * @author zls * @date 2020/4/4 */ public class SyncStaticMethodTest { public static void main(String[] args) { Thread t1 = new Thread(() -> Foo.dosome()); Thread t2 = new Thread(() -> Foo.dosome()); t1.start(); t2.start(); } } class Foo { @SneakyThrows public synchronized static void dosome() { Thread t = Thread.currentThread(); System.out.println(t.getName() + ":正在运行dosome方法"); Thread.sleep(5000); System.out.println(t.getName() + ":运行dosome方法完毕!"); } }
1.4 互斥锁
当synchronized修饰两段不同代码,但是同步监视器对象相同时,这两段代码就具有了互斥性,多段代码也可以。
package thread; import lombok.SneakyThrows; /** * 互斥锁 * * @author zls * @date 2020/4/4 */ public class SyncMutexTest { public static void main(String[] args) { final Boo b = new Boo(); Thread t1 = new Thread(() -> b.methodA()); Thread t2 = new Thread(() -> b.methodB()); t1.start(); t2.start(); } } class Boo { @SneakyThrows public synchronized void methodA() { Thread t = Thread.currentThread(); System.out.println(t.getName() + ":正在运行A方法"); Thread.sleep(5000); System.out.println(t.getName() + ":A方法运行完毕"); } @SneakyThrows public synchronized void methodB() { Thread t = Thread.currentThread(); System.out.println(t.getName() + ":正在运行B方法"); Thread.sleep(5000); System.out.println(t.getName() + ":B方法运行完毕"); } }
1.5 死锁
两个线程都在等待对方先施放锁。
package thread; import lombok.SneakyThrows; /** * 死锁测试 * 解释:线程1和线程2永远不会有执行结果。线程1调用了c.A()方法而在A()方法里面又调用了B()方法, * 而线程2调用了c.B()方法,B()方法里面又调用了A()方法,这样当线程1中A方法里面调用B方法的时候, * 此时B()方法,被线程2持有,线程1的A()方法无法调用B()方法,只有等到线程2的B()方法执行完释放了锁, * 线程1的A()方法才能调用,因为不能调用所以,线程1就形成了等待(也可以说成是阻塞)。反之亦然,线程2也是 * 在等待线程1执行完A()方法,所以线程2也是形成了等待(阻塞)。 * 形象的说,这就好比两个人过独木桥,都走到了中间,结果谁也不让让谁,试问谁能过桥? * * @author zls * @date 2020/4/4 */ public class SyncDieLockTest { public static void main(String[] args) { final Coo c = new Coo(); Thread t1 = new Thread(() -> c.methodA()); Thread t2 = new Thread(() -> c.methodB()); t1.start(); t2.start(); } } class Coo { private Object oa = new Object(); private Object ob = new Object(); @SneakyThrows public void methodA() { synchronized (oa) { System.out.println(Thread.currentThread().getName() + ":正在执行A方法.."); Thread.sleep(5000); methodB(); System.out.println(Thread.currentThread().getName() + "运行A方法完毕"); } } @SneakyThrows public void methodB() { synchronized (ob) { System.out.println(Thread.currentThread().getName() + ":正在执行B方法.."); Thread.sleep(5000); methodA(); System.out.println(Thread.currentThread().getName() + "运行B方法完毕"); } } }
1.6 方法锁、对象锁、类锁
java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。
线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁,获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,
类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
synchronized 修饰代码块时,需要一个引用对象作为锁的对象;
修饰方法时,默认是当前对线作为锁的对象;
修饰类时,默认是当前类的Class对象作为锁的对象;
对象锁:
当一个对象中有同步方法或者同步块,线程调用此对象进入该同步区域时,必须获得对象锁。如果此对象的对象锁被其他调用者占用,
则进入阻塞队列,等待此锁被释放(同步块正常返回或者抛异常终止,由JVM自动释放对象锁)。
注意: 方法锁也是一种对象锁,当一个线程访问一个带synchronized方法时,由于对象锁的存在,所有加synchronized的方法都不能被访问(前提是在多个线程调用的是同一个对象实例中的方法)。
非静态方法上的锁,用this做锁;
引用对象作为锁,用类中的成员变量引用做锁。
类锁:
一个class其中的静态方法和静态变量在内存中只会加载和初始化一份,所以,一旦一个静态的方法被申明为synchronized,
此类的所有的实例化对象在调用该方法时,共用同一把锁,称之为类锁。
类锁对该类的多有对象都能起作用,而对象锁不能。类中静态方法上的锁,用xxx.class做锁。
1.7 线程安全的API
StringBuffer是同步的synchronized append();
StringBuilder不是同步的append();
Vector和Hashtable是同步的;
ArrayList和HashMap不是同步的;
获取线程安全的集合方式:
a.获取线程安装的List集合:
Collections.synchronizedList()
b.获取线程安全的Map:
Collections.synchronizedMap()
2. 线程池
线程池:线程池指的是多个线程的集合。
为什么要使用线程池?
当一个程序中若创建大量线程,并在任务结束后就销毁,会给系统带来过度的资源消耗, 以及过度切换线程的危险,从而可能导致系统崩溃。
ExecutorServices是java提供的用于管理线程池的类。
线程池的两个主要作用:
a. 控制线程数量
b. 重用线程
机理:
在服务器接收到客户端发送过来的请求后,它会将任务交给整个线程池,而线程池在拿到任务以后,它会在内部找到一个空闲的线程,然后由这个线程为之服务。服务完后不关闭该线程,而是将该线程归还到线程池中,以便下一次的使用。这里的线程池和连接池很类似。一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
线程池有以下几种实施策略:
a. 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们;
Executors.newCachedThreadPool()
b. 创建一个可重用固定线程集合的线程池,以共享的无界队列方式俩运行这些线程;
Executors.newScheduledThreadPool(int corePoolSize)
3. 队列的使用
3.1 BlockingQueue
BlockingQueue(阻塞队列)是Queue接口的一个子接口,是为线程之间共享数据而设计的。 BlockingQueue是一个支持两个附加操作的队列。这两个附加的操作是:
(1) 在队列为空时,获取元素的线程会等待队列变为非空;
(2) 当队列满时,存储元素的线程会等待队列可用,从而产生阻塞。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的缓存容器,而消费者也只从容器里拿元素。
在多线程并发时,若需要使用队列,我们可以使用Queue,但是需要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
抛出异常 | 特殊值 | 阻塞 | 超时 | |
插入 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除 | remove() |
poll() |
take() |
poll(time, unit) |
检查 | element() |
peek() |
不可用 | 不可用 |
BlockingQueue特性:
(1) 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
(2) 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告Integer.MAX_VALUE 的剩余容量。
(3) 实现主要用于生产者-使用者队列,但它另外还支持 Collection 接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不 会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
(4) 实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection 操作(addAll、containsAll、retainAll 和removeAll)没有必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c 中的一些元素后,addAll(c) 有可能失败(抛出一个异常)。
BlockingQueue的实现类有:ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue;
子接口有:
public interface BlockingDeque extends BlockingQueue, Deque 1.6新增
public interface TransferQueue extends BlockingQueue 1.7新增
eg1: 生产者/消费者模型
解释:当生产者与消费者线程启动后,首先生产者会不断往队列中添加产品,一旦队列填满则生产停止,然后消费者从队列中取出产品使用。
package producer_consumer; import java.util.Random; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; /** * 生产者消费者模型的作用是什么? * (1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用 * (2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约 * <p> * 写一个生产者-消费者队列 * 可以通过阻塞队列实现,也可以通过wait-notify来实现. * 以下是通过使用阻塞队列来实现: * <p> * 原文链接:https://blog.csdn.net/qq_41701956/article/details/86773940 * * @author zls * @date 2020/2/4 */ public class MyTest { public static void main(String[] args) { BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(100); // 一个生产者,两个消费者 Producer p = new Producer(queue); Consumer c1 = new Consumer(queue); Consumer c2 = new Consumer(queue); new Thread(p).start(); new Thread(c1).start(); new Thread(c2).start(); } } /** * 生产者 */ class Producer implements Runnable { // 阻塞队列 private final BlockingQueue<Integer> queue; public Producer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { while (true) { Thread.sleep(1000);//模拟耗时 queue.put(produce()); //将产生的随机数放入队列 } } catch (InterruptedException e) { } } /** * 生产方法:这里产生的随机数 * * @return */ private int produce() { int n = new Random().nextInt(10000); System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n); return n; } } /** * 消费者 */ class Consumer implements Runnable { private final BlockingQueue<Integer> queue; public Consumer(BlockingQueue q) { this.queue = q; } @Override public void run() { while (true) { try { Thread.sleep(2000);//模拟耗时 consume(queue.take());//从队列中取出随机数 } catch (InterruptedException e) { } } } /** * 消费方法:这里消耗随机数 */ private void consume(Integer n) { System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n); } }
四、应用场景
eg1: 假设用户向服务端发送一个请求,这个请求需要执行2两个很慢的io操作(eg数据库查询或者文件读取)
eg2:多个售票窗口售票的问题?
同步代码块的使用,限制不同的窗口访问/修改同一票源.
/** * 多个售票窗口售票的问题? * 程序分析: * (1)票数要使用同一个静态值 * (2)为保证不会出现卖出同一个票数,要java多线程同步锁。 * 设计思路: * (1)创建一个站台类Station,继承Thread,重写run方法,在run方法里面执行售票操作!售票要使用同步锁:即有一个站台卖这张票时,其他站台要等这张票卖完! * (2)创建主方法调用类 * @author zls * @date 2020/2/4 */ public class TheardStationDemo { public static void main(String[] args) { /** * java多线程同步锁的使用 * 示例:三个售票窗口同时出售10张票 */ Station station1 = new Station("窗口1"); //创建三个先代表三个站台,并为每一个站台取名字 Station station2 = new Station("窗口2"); Station station3 = new Station("窗口3"); // 让每一个站台对象各自开始工作 station1.start(); station2.start(); station3.start(); } } /** * 站台类 */ class Station extends Thread { // 通过构造方法给线程名字赋值 public Station(String name) { super(name); } static int tick = 10; // 为了保持票数的一致,票数要静态 static Object ob = "aa";// 创建一个静态钥匙,值是任意的 // 重写run方法,实现买票操作 @Override public void run() { while (tick > 0) { synchronized (ob) {// 这个很重要,必须使用一个锁,进去的人会把钥匙拿在手上,出来后才把钥匙拿让出来 if (tick > 0) { System.out.println(getName() + "卖出了第" + tick + "张票"); tick--; } else { System.out.println("票卖完了"); } } try { sleep(1000);//休息一秒 } catch (InterruptedException e) { e.printStackTrace(); } } } }
eg3: 两个人AB通过一个账户A在柜台取钱和B在ATM机取钱?
同步方法的使用,限制不同的取款方式调用同一取款方法.
/** * 问题:两个人AB通过一个账户A在柜台取钱和B在ATM机取钱? * 程序分析: * 钱的数量要设置成一个静态的变量,两个人要取的同一个对象值。 * <p> * 参考:https://blog.csdn.net/qq_34996727/article/details/80416277 * * @author zls * @date 2020/2/4 */ public class BankDemo { public static void main(String[] args) { Bank bank = new Bank(); // 实例化两个人,传入同一个银行的对象 Person a = new Person(bank, "counter"); Person b = new Person(bank, "ATM"); a.start(); b.start(); } } class Bank { // 假设一个账户有1000块钱 static double money = 1000; /** * 柜台Counter取钱的方法 */ private void counter(double money) { Bank.money -= money; System.out.println("柜台取钱" + money + "元,还剩" + Bank.money + "元!"); } /** * ATM取钱的方法 */ private void ATM(double money) { Bank.money -= money; System.out.println("ATM取钱" + money + "元,还剩" + Bank.money + "元!"); } /** * 提供一个对外取款途径,防止直接调取方法同时取款时,并发余额显示错误 * * @param money * @param mode 采用哪种方式取钱 * @throws Exception */ public synchronized void outMoney(double money, String mode) throws Exception { if (money > Bank.money) { //校验余额是否充足 throw new Exception("取款金额" + money + ",余额只剩" + Bank.money + ",取款失败"); } if (Objects.equals(mode, "ATM")) { ATM(money); } else { counter(money); } } } class Person extends Thread { Bank bank; String mode; public Person(Bank bank, String mode) { this.mode = mode; this.bank = bank; } public void run() { while (bank.money >= 100) { try { bank.outMoney(100, mode); } catch (Exception e1) { e1.printStackTrace(); } try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
eg4: 龟兔赛跑问题?
线程的回调问题
package ning.myThread; /** * 龟兔赛跑问题? * 龟兔赛跑:200米 * 要求: * (1)兔子每 0.1 秒 5 米的速度,每跑20米休息1秒; * (2)乌龟每 0.1 秒跑 2 米,不休息; * (3)其中一个跑到终点后另一个不跑了! * 程序设计思路: * (1)创建一个Animal动物类,继承Thread,编写一个running抽象方法,重写run方法,把running方法在run方法里面调用。 * (2)创建Rabbit兔子类和Tortoise乌龟类,继承动物类 * (3)两个子类重写running方法 * (4)本题的第3个要求涉及到线程回调。需要在动物类创建一个回调接口,创建一个回调对象。 * <p> * 原文链接:https://blog.csdn.net/qq_34996727/article/details/80416277 * * @author zls * @date 2020/2/4 */ public class AnimalDemo { public static void main(String[] args) { Tortoise tortoise = new Tortoise(); Rabbit rabbit = new Rabbit(); // 回调方法的使用,谁先调用calltoback方法,另一个就不跑了 LetOneStop tortoiseCalltoback = new LetOneStop(tortoise); // 让兔子的回调方法里面存在乌龟对象的值,可以把乌龟stop rabbit.calltoback = tortoiseCalltoback; LetOneStop rabbitCalltoback = new LetOneStop(rabbit); // 让乌龟的回调方法里面存在兔子对象的值,可以把兔子stop tortoise.calltoback = rabbitCalltoback; // 开始跑 tortoise.start(); rabbit.start(); } } /** * 创建Animal动物类 */ abstract class Animal extends Thread { public int length = 200;// 比赛长度 public abstract void runing(); @Override public void run() { super.run(); // 控制当前线程何时停止 while (length > 0) { runing(); } } /** * 在需要回调数据的地方(两个子类需要),声明一个接口 */ static interface Calltoback { public void win(); } // 2.创建接口对象 Calltoback calltoback; } /** * 兔子类 */ class Rabbit extends Animal { public Rabbit() { setName("兔子"); } @Override public void runing() { //兔子速度 int dis = 5; length -= dis; System.out.println("兔子跑了" + dis + "米,距离终点还有" + length + "米"); //兔子跑完了 if (length <= 0) { length = 0; System.out.println("兔子获得了胜利"); // 给回调对象赋值,让乌龟不要再跑了 if (calltoback != null) { calltoback.win(); } } try { if ((2000 - length) % 20 == 0) { // 每20米休息一次,休息时间是1秒 sleep(1000); } else { //没0.1秒跑5米 sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 乌龟跑完了 */ class Tortoise extends Animal { public Tortoise() { setName("乌龟");// Thread的方法,给线程赋值名字 } // 重写running方法,编写乌龟的奔跑操作 @Override public void runing() { // 乌龟速度 int dis = 2; length -= dis; System.out.println("乌龟跑了" + dis + "米,距离终点还有" + length + "米"); // 乌龟跑完了 if (length <= 0) { length = 0; System.out.println("乌龟获得了胜利"); // 让兔子不要在跑了 if (calltoback != null) { calltoback.win(); } } try { sleep(100); //每0.1秒跑2米 } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 创建一个让动物线程停止的类,这里要实现回调接口 */ class LetOneStop implements Animal.Calltoback { // 动物对象 Animal an; public LetOneStop(Animal an) { this.an = an; } // 让动物的线程停止 @Override public void win() { // 线程停止 an.stop(); } }
参考文档:
https://www.cnblogs.com/wxd0108/p/5479442.html
https://www.cnblogs.com/xdp-gacl/p/3633936.html
https://blog.csdn.net/qq_41701956/article/details/86773940