明确一点:多线程不是为了提高程序执行速度(性能甚至更低),而是提高应用程序的使用效率。

多线程的三大特性:原子性、可见性、有序性

一、创建线程

创建线程额的开销:分配内存 --> 列入调度 --> 线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息(破坏了数据的局部性)

创建线程的三种方式

1.继承Thread(重点),重写run()方法,在main函数中,调用start()方法
2.实现Runnable接口(重点),重写run()方法,在main函数中,调用start()方法
3.实现Callable接口(了解),重写call()方法,在main函数中调用start()方法

1.继承Thread类

package com.yang.demo01;
/*
继承Thead类
重新run方法
在main中调用start开启线程*/
public class TestThread01 extends Thread{@Overridepublic void run() {//run方法线程体,该线程要执行的操作for (int i = 0; i < 200; i++) {System.out.println("灰太狼");}}public static void main(String[] args) {//创建线程对象并调用start方法new TestThread01().start();for (int i = 0; i < 1000; i++) {System.out.println("美羊羊");}}
}

发现结果是乱序的(而且每次的运行结果都一样),主线程和子线程是并行交替执行的,实际的运行是根据CPU的分配情况来决定的!

美羊羊
美羊羊
美羊羊
灰太狼
灰太狼
...
美羊羊

程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用start()方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

要注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的!

而run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。

复杂一点的:

2.实现Runnbale接口

package com.yang.demo01;
/*
实现Runnbale接口
重写run方法*/
public class TeastRunnable implements Runnable{@Overridepublic void run() {//run方法线程体for (int i = 0; i < 200; i++) {System.out.println("灰太狼");}}public static void main(String[] args) {//创建runnable接口的实现类对象TestThread01 testThread01 = new TestThread01();//创建线程对象,通过线程对象来开启线程new Thread(testThread01).start();for (int i = 0; i < 1000; i++) {System.out.println("美羊羊");}}
}

结果也是交替的!

那这两种方式哪种比较好?

继承Thread类不适合资源共享,实现Runnable接口更具有灵活性。

继承Thread:


public class FirstThread extends Thread {public void run() {for (int i = 1; i <= 10; i++) {System.out.println(getName() + " " + i);}}public static void main(String args[]) {for (int i = 1; i <= 5; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}new FirstThread().start();new FirstThread().start();}
}

实现Runnable接口:

package com.yang.demo01;public class Test01 implements Runnable{private int ticketNums = 10;@Overridepublic void run() {while (true){if (ticketNums <= 0){break;}try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums-- +"票");}}public static void main(String[] args) {Test01 ticket = new Test01();
/*      Thread thread01 = new Thread(t1,"a");thread01.start();Thread thread02 = new Thread(t1,"b");thread02.start();*/new Thread(ticket,"喜洋洋").start();new Thread(ticket,"灰太狼").start();new Thread(ticket,"机器猫").start();}
}
对比    new FirstThread().start();new FirstThread().start();
和Test01 ticket = new Test01();new Thread(ticket,"喜洋洋").start();new Thread(ticket,"灰太狼").start();new Thread(ticket,"机器猫").start();

可以发现传给Thread的是同一个Runnable对象,这也就是为什么说实现Runnable接口,可以资源共享,因为操作的都是同一个对象!而继承Thread每次都要new一个新的对象,对象自然就不同了。并且,实现Runnable接口这种方式更体现了面向对象这种思维,new一个线程,线程里面传一个对象,这个对象封装了一系列操作。

所以总的来说,好处大致有四点

1.避免继承的局限!因为一个类可以实现多个接口。
2.适合于资源的共享。
3.线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类。
4.增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

二、线程状态

线程的五种状态:
新建状态、就绪状态、运行状态、阻塞状态、死亡状态 。



详细解释:
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法时进入就绪状态。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)等待阻塞:运行的线程执行 wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)同步阻塞:运行的线程在获取对象的 同步锁 时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)其他阻塞:运行的线程执行 sleep()或join()方法 ,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

三、线程状态的常用方法

1. 如何让线程停止?
可以使用一个标志位,进行终止变量,当flag=false时,则线程停止。

为什么不能使用JDK提供的stop()和suspend()方法?
答:stop() 会解除由线程获取的所有锁定,是不安全的。
       suspend()方法容易发生死锁。因为调用 suspend()的时候, 目标线程会停下来, 但却仍然持有在这之前获得的锁定。

package com.yang;public class Stop implements Runnable{//设置一个标志位private boolean flag = true;@Overridepublic void run() {int i = 0;while (flag){System.out.println("run Thread "+ i++);}}//自己设置一个stop方法来转换标志位,停止线程public void stop(){this.flag = false;}public static void main(String[] args) {Stop stop = new Stop();new Thread(stop).start();for (int i = 0; i<1000; i++){System.out.println("main线程 "+ i);;if (i == 900){//调用stop方法切换标志位,让线程停止stop.stop();System.out.println("子线程停止");}}}
}

2. 线程休眠:sleep()

指定当前进程阻塞的毫秒数。sleep存在一个InterruptedException异常,当sleep时间到达后,线程进入就绪状态。一般run()里面都要加一个sleep()。要注意:每个对象都有一个锁,而sleep并不会释放锁!

sleep可以用来做倒计时。

package com.yang;
//模拟倒计时
public class Sleep  {public static void tenCount() throws InterruptedException {int nums = 10;while (true){Thread.sleep(1000);  //休眠一秒System.out.println(nums--);if (nums <= 0){break;}}}}

还可以用来获取定时系统时间:

package com.yang;import java.text.SimpleDateFormat;
import java.util.Date;//模拟倒计时
public class Sleep  {public static void main(String[] args) {Date time = new Date(System.currentTimeMillis());// 获取系统时间while (true){try {Thread.sleep(2000);  //休眠两秒System.out.println(new SimpleDateFormat("HH:mm:ss").format(time)); //日期格式化,输出时间time = new Date(System.currentTimeMillis());// 更新时间} catch (InterruptedException e) {e.printStackTrace();}}}

结果:

2021-02-16 22:23:01
2021-02-16 22:23:03
2021-02-16 22:23:05
2021-02-16 22:23:07
2021-02-16 22:23:09
2021-02-16 22:23:11
...

3. 线程礼让:yield()

让当前正在执行的线程暂停,但不会让线程转到等待/睡眠/阻塞状态!只是让线程从运行状态转为就绪状态,把执行机会让给相同或者更高优先级的线程,让CPU重新调度。礼让不一定能成功,还是要看CPU。也就是说,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

package com.yang;public class Yield implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"线程开始执行");Thread.yield();  //礼让System.out.println(Thread.currentThread().getName()+"线程停止执行");}public static void main(String[] args) {Yield yield = new Yield();new Thread(yield,"a").start();new Thread(yield,"b").start();}
}

如果不加礼让时:

a线程开始执行
a线程停止执行
b线程开始执行
b线程停止执行

加了礼让时:

//礼让失败
a线程开始执行
a线程停止执行
b线程开始执行
b线程停止执行
//礼让成功
a线程开始执行
b线程停止执行
a线程开始执行
b线程停止执行

sleep()和yield()的区别:

  1. 状态。sleep()使当前线程进入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

  2. 时间。sleep 方法使当前运行中的线程睡眠一段时间,进入阻塞状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

  3. 优先级。sleep()方法在给其他线程运行机会时不考虑线程的优先级,低优先级仍有机会被调度。而yield()方法只会给相同优先级或更高优先级的线程运行的机会。如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

4. 等待线程终止:join()

join()的作用是:“等待该线程终止”。待该线程执行完成后,才能执行其他线程。否则其余线程只能阻塞。这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。可以理解为插队!和sleep一样,也会抛出InterruptedException异常。

package com.yang;public class Join implements Runnable {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("vip线程"+i);}}public static void main(String[] args) throws InterruptedException {Join join = new Join();Thread thread = new Thread(join);thread.start();//主线程for (int i = 0; i <10 ; i++) {if (i == 4){thread.join(); //让vip线程插队}System.out.println("main"+i);}}
}

结果:

main0
main1
main2
main3
vip线程0
vip线程1
vip线程2
vip线程3
vip线程4
main4
main5
main6
main7
main8
main9
Process finished with exit code 0

为什么要用join方法?或者说什么时候用join方法?

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

5. 线程通信中的方法:wait()和notify()

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作。它们都属于Object类。

5.1 强迫一个线程等待:wait()
  当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的锁(暂时失去锁,wait(long timeout)超时时间到后会返还对象锁);其他线程可以访问;
使用wait之后,必须使用notify或者notifyAll来唤醒当前等待池中的线程。
也就是说,使用wait后,线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行

5.2 唤醒一个线程:notify()
  与wait()对应,notify()是释放自身对象锁,唤醒下一个等待线程。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的同步代码块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。

案例:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

package com.yang.state;public class wait implements Runnable {private String name;private Object prev;private Object self;wait(String name,Object prev, Object self){this.name = name;this.prev = prev;this.self = self;}@Overridepublic void run() {int count = 10;while (count > 0) {synchronized (prev) {synchronized (self) {System.out.print(name);count--;self.notify(); //释放自身对象(self)锁,唤醒下一个等待线程}try {prev.wait(); //释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) throws InterruptedException {//Object对象Object a = new Object();Object b = new Object();Object c = new Object();wait pa = new wait("A", c, a);wait pb = new wait("B", a, b);wait pc = new wait("C", b, c);new Thread(pa).start();Thread.sleep(1000);  //确保按A、B、C顺序执行new Thread(pb).start();Thread.sleep(1000);new Thread(pc).start();Thread.sleep(1000);}
}

结果:

 该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。

面试题:sleep()和wait()的区别?

答:1. sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。 
  2. sleep()是Thread类的方法,wait()是Object的方法
  3.wait()只能在同步方法或者同步代码块里面使用,而sleep()可以在任何地方使用
  4.sleep只有睡够时间才能醒,wait可以随时唤醒

四、线程优先级

Java线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10。

Thread.MIN_PRIORITY=1     线程可以具有的最低优先级
Thread.MAX_PRIORITY=10    线程可以具有的最高优先级
Thread.NORM_PRIORITY=5    分配给线程的默认优先级,取值为5

可以使用getPriority()和setPriority(int xxx)来改变和获取优先级。

package com.yang;public class Priority implements Runnable{@Overridepublic void run() {//查看主线程的默认优先级System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());}public static void main(String[] args) {System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());Priority priority = new Priority();Thread thread1 = new Thread(priority);Thread thread2 = new Thread(priority);Thread thread3 = new Thread(priority);//不设置优先级thread1.start();//先设置优先级再启动thread2.setPriority(1);thread2.start();thread3.setPriority(7);thread3.start();}
}

结果:

main-->5
Thread-2-->7
Thread-0-->5
Thread-1-->1Process finished with exit code 0

但是要注意,优先级低只意味着获得调度的概率低!并不是一定按着优先级的高低来调用的!这都是看CPU的调度。

main-->5
Thread-0-->5
Thread-2-->6
Thread-1-->1Process finished with exit code 0

五、不同线程解释

(了解即可,重点就一个用户线程和守护进程的区别)

主线程:JVM调用程序main()所产生的线程。

当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。

守护(daemon)线程:指为其他线程提供服务的线程,也称为后台线程。JVM的垃圾回收线程(gc)就是一个守护线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为守护线程。 线程分为 用户线程守护线程 ,他们的区别在于:是否等待主线程依赖于主线程结束而结束。

用户线程线程:正常的线程都是用户线程。

六、线程同步

1.概念

处理多线程问题时,多个线程访问同一个对象(并发问题),而且某些线程还想修改这个对象,这个时候我们就需要线程同步。线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕之后,下一个线程再使用。
而线程同步的形成条件就是:队列+锁。比如上排队上厕所,进去之后,只能把厕所门锁上,才能保证里面安全,否则后面排队的全都进去的话,,就不安全了。。也就是说,为了解决冲突问题,在访问时加入锁机制,当一个线程获得对象的排他锁,独占资源时,其他线程必须等待,等这个线程使用之后释放锁。所以这也导致了以下问题:

(1).一个线程持有锁时会导致其他需要此锁的线程挂起。

(2).在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度演示,引起性能问题(这是必然的,就像你进厕所之后把门锁了,别人只能等着。虽然安全了,但是比起一起进厕所,性能低了)。

(3).如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

不安全案例:买票

package com.yang.syn;public class BuyTickets implements Runnable {private int tickets = 10;boolean flag = true;public static void main(String[] args) {BuyTickets tickets = new BuyTickets();new Thread(tickets,"喜羊羊").start();new Thread(tickets,"灰太狼").start();new Thread(tickets,"村长").start();}@Overridepublic void run() {while (flag){buy();}}//买票方法private void buy(){//判断是否有票if (tickets <= 0){flag = false;return;}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"买到第"+tickets--+"张票");}
}

发现出现了票重复的现象,比如有两个第九张票。。。这就是线程不安全!就需要用到同步技术。

灰太狼买到第9张票
喜羊羊买到第10张票
村长买到第10张票
喜羊羊买到第8张票
灰太狼买到第7张票
村长买到第6张票
村长买到第5张票
灰太狼买到第4张票
喜羊羊买到第5张票
灰太狼买到第3张票
村长买到第2张票
喜羊羊买到第3张票
村长买到第1张票
灰太狼买到第1张票
喜羊羊买到第1张票Process finished with exit code 0

2、同步方法和同步块:

synchronized关键字有两种用法:synchronized方法和synchronized块。

(1).synchronized方法:

public synchronized void method(int args){}

  synchronized方法控制对对象的访问,每个对象对应一把锁。synchronized方法必须获得调用该方法的对象的锁才能执行。而且一旦执行,就独占该锁,直至方法运行结束释放锁,后面被阻塞的线程才能获得这个锁继续执行。在某个对象实例内,synchronized 方法可以防止多个线程同时访问这个对象的synchronized方法。也就是说,如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程就不能同时访问这个对象中任何一个synchronized方法。(牢记:锁的是对象,无论synchronized关键字加在方法上还是对象上。)

同步方法有锁吗?
有锁,是本类的对象引用,this。

PS:这里还有个面试题:当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

答:不能。其它线程只能访问该对象的非同步方法,试图进入B方法的线程就只能在等锁池(注意不是等待池)中等待对象的锁。

解决买票问题,只需要在方法中加一个synchronized关键字就好,就实现了队列+锁

package com.yang.syn;public class BuyTickets implements Runnable {private int tickets = 10;boolean flag = true;public static void main(String[] args) {BuyTickets tickets = new BuyTickets();new Thread(tickets,"喜羊羊").start();new Thread(tickets,"灰太狼").start();new Thread(tickets,"村长").start();}@Overridepublic void run() {while (flag){buy();}}//synchronized 同步方法,锁的是对象本身,或者说是调用这个同步方法的对象,即this。private synchronized void buy(){//判断是否有票if (tickets <= 0){flag = false;return;}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"买到第"+tickets--+"张票");}
}

结果:

喜羊羊买到第10张票
村长买到第9张票
灰太狼买到第8张票
村长买到第7张票
喜羊羊买到第6张票
村长买到第5张票
灰太狼买到第4张票
村长买到第3张票
村长买到第2张票
村长买到第1张票

 如果将synchronized作用于静态(static) 函数时,取得的锁是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。即 本类类名.class

(2).synchronized块:

synchronized(Obj){/*区块*/}

Obj称为同步监视器:

  • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无序指定同步监视器,因为同步方法的同步监视器就是this,即这个对象本身或者是class(反射)。

同步监视器执行过程:

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没有锁,锁定并访问。

PS:这里也有个面试题:在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

答:在 java 虚拟机中, 每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都对应着一把锁. 一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码 。 java 提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案。程序员可以使用显示锁或隐式锁实现互斥,wait notify notifyall condition 实现协作。

实例:线程不安全的集合

package com.yang.syn;import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {public static void main(String[] args) {List<Object> list = new ArrayList<>();for (int i = 0; i < 1000 ; i++) {//开启线程new Thread(()->{//向集合中添加线程名字list.add(Thread.currentThread().getName());}).start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}//查看集合大小System.out.println(list.size());}}
}

正常情况来说,集合中应该有1000个线程名字。但是事实是,每次运行结果都不一样。这是因为不同线程可能在同一时刻将名字添加到了同一位置,出现了覆盖。这也是线程不安全的。
(其实java中有一个JUC安全类型的集合,叫CopyOnWriteArrayList()

解决:同步代码块

package com.yang.syn;import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {public static void main(String[] args) {List<Object> list = new ArrayList<>();for (int i = 0; i < 10000 ; i++) {//开启线程new Thread(()->{//增加同步代码块,把list锁住(锁的一般是需要变化的量,即需要增删改的对象)synchronized (list){//向集合中添加线程名字list.add(Thread.currentThread().getName());}}).start();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//查看集合大小System.out.println(list.size());}
}

此时锁就是list这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序。

同步方法和同步代码块的区别是什么?

答:同步方法默认用this或者当前类class对象作为锁;
  同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。

3.死锁

package com.yang.syn;public class DeadLock extends Thread{//构造方法DeadLock(int choice){this.choice=choice;}//用static来保证只有一份资源static A a = new A();static B b = new B();int choice;public static void main(String[] args) {//创建线程DeadLock deadLock1 = new DeadLock(0);DeadLock deadLock2 = new DeadLock(1);deadLock1.start();deadLock2.start();}@Overridepublic void run() {if (choice == 0){synchronized (a){   //获得a的锁System.out.println(Thread.currentThread().getName()+"if...lockA");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (b){  //一秒后获得b的锁System.out.println(Thread.currentThread().getName()+"if...LockB");}}}else {synchronized (b){  //获得b的锁System.out.println(Thread.currentThread().getName()+"else..LockB");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (a){  //一秒后获得a的锁System.out.println(Thread.currentThread().getName()+"else..LockA");}}}}}class A{}
class B{}

结果:卡死,陷入死锁。

这是因为a和b两个对象锁时唯一性的,即当一个线程走if语句获得a锁后,else中的a锁也没有了。。b锁同理。所以会陷入死锁。

死锁的前提:必须是多线程,而且出现同步嵌套!如果不把同步代码块进行嵌套,就不会死锁。比如:

package com.yang.syn;public class DeadLock extends Thread{//构造方法DeadLock(int choice){this.choice=choice;}//用static来保证只有一份资源static A a = new A();static B b = new B();int choice;public static void main(String[] args) {//创建线程DeadLock deadLock1 = new DeadLock(0);DeadLock deadLock2 = new DeadLock(1);deadLock1.start();deadLock2.start();}@Overridepublic void run() {if (choice == 0){synchronized (a){   //获得a的锁System.out.println(Thread.currentThread().getName()+"if...lockA");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//不嵌套在a的同步代码块中synchronized (b){  //一秒后获得b的锁System.out.println(Thread.currentThread().getName()+"if...LockB");}}else {synchronized (b){  //获得b的锁System.out.println(Thread.currentThread().getName()+"else..LockB");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//不嵌套在b的同步代码块中synchronized (a){  //一秒后获得a的锁System.out.println(Thread.currentThread().getName()+"else..LockA");}}}}//
class A{}
class B{}

4.Lock锁

JDK5引入ReentrantLock(可重入锁)类。显示的定义锁!

package com.yang.lock;import com.yang.syn.BuyTickets;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class TestLock implements Runnable{private int tickets = 10;boolean flag = true;public static void main(String[] args) {BuyTickets tickets = new BuyTickets();new Thread(tickets,"喜羊羊").start();new Thread(tickets,"灰太狼").start();new Thread(tickets,"村长").start();}//定义lock锁private final Lock lock = new ReentrantLock();@Overridepublic void run() {while (flag){try {lock.lock(); //加锁if (tickets <= 0){flag = false;return;}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"买到第"+tickets--+"张票");}finally {//解锁lock.unlock();}}}
}

synchronized和Lock对比:

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
  • Lock只代码块锁,synchonized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好

(但是最长用的其实是synchronized)。

以上,就是能实现线程同步的所有方法。即:
 (1)同步方法:synchronized关键字修饰的方法
 (2)同步代码块:synchronized修饰的代码块
 (3)显示同步锁:ReentrantLock(可重入锁)类中的Lock
 (4)特殊域变量(volatile)实现线程同步(不太清楚这个,应该不常用)

总结:
(1)、线程同步的目的是为了防止多个线程访问一个资源时对资源的破坏。

( 2)、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。

(3)、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
(4)、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

七、线程池

在经常创建和销毁、使用量特别大的资源,比如并发情况下得线程,对性能影响很大。这个时候就可以使用线程池。提前创建好多个线程,放入线程池中,使用时直接获取,使用完就放回池中。这样可以避免频繁的创建销毁、实现重复利用。
好处是:
1、提高响应速度
2、降低资源消耗
3、便于线程管理

但是用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足线程泄漏

JDK5起提供了线程池相关的API:ExecutorService和Executors

  • ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程
  • Executors是工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

四种常见的线程池(返回值都是ExecutorService):

1.Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务

2.Executors.newFixedThreadPool(int n): 创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

3.Executors.newScheduledThreadPool(int n):单线程化的Executor,即只创建唯一的工作者线程执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。

4.Executors.newScheduleThreadPool: 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

示例:

package com.yang.Pool;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestPool  implements Runnable{public static void main(String[] args) {//创建线程池ExecutorService service = Executors.newFixedThreadPool(10);//执行service.execute(new TestPool());service.execute(new TestPool());service.execute(new TestPool());service.execute(new TestPool());//关闭连接service.shutdown();}@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}
}

结果:

java多线程详细理解相关推荐

  1. Java 多线程 —— 深入理解 volatile 的原理以及应用

    转载自  Java 多线程 -- 深入理解 volatile 的原理以及应用 推荐阅读:<java 多线程-线程怎么来的> 这一篇主要讲解一下volatile的原理以及应用,想必看完这一篇 ...

  2. Java多线程再理解(synchronized)

    2019独角兽企业重金招聘Python工程师标准>>> synchronized 实现原理 synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临 ...

  3. Java多线程深入理解学习笔记之二-----多线程实现方案1及方法简介

    多线程的实现方案1: 继承Thread类 重写run()方法 在测试类中使用start()方法 贴一下代码(自定义线程): package textDemo;public class MyThread ...

  4. JAVA多线程详细讲解

    一个程序只有一个从头到尾的执行路径.这样做的优点是易于编程,无需考虑过多的情况.但是,由于单线程需要在上一个任务完成之后才开始下一个任务,所以其效率比较低.在真实的项目运行过程中都具有多任务同时执行的 ...

  5. Java线程、Java多线程详细介绍

    目录 一.进程和线程的区别 1.1 进程 1.2 线程 二.并发和并行 2.1 并行 2.2  并发 2.3 监控线程的执行情况 三.创建方式 3.1 继承Thread类 思考:为什么不直接通过对象调 ...

  6. java多线程详细讲解_Java多线程例子讲解

    一:知识点声明: 1.区别进程和线程:进程是静态概念,它的执行依赖线程进行. 2.进程的状态:就绪(等待cpu执行),运行,中止,阻塞(等待所需资源,进入阻塞态) 3.Java程序的main函数即是一 ...

  7. Java多线程 -- 深入理解JMM(Java内存模型) --(五)锁

    [转载自并发编程网 – ifeve.com 原文链接:http://ifeve.com/tag/jmm/] 锁的释放-获取建立的happens before 关系 锁是java并发编程中最重要的同步机 ...

  8. Java多线程学习详细学习及扩展

    Java多线程详细学习及扩展 学习视频:b站狂神–多线程篇 一.概述 1.1.名词解释 程序(application):程序是指令和数据的有序集合,其本身没有任何的运行的含义,是一个静态的概念. 进程 ...

  9. 转:Java多线程学习(总结很详细!!!)

    Java多线程学习(总结很详细!!!) 此文只能说是java多线程的一个入门,其实Java里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更上一个台阶呢? 本文主要讲java中多线程 ...

最新文章

  1. 线性O(N)时间复杂度求素数 , 筛法
  2. 【Spring】Spring系列6之Spring整合Hibernate
  3. JQUERY GET
  4. ecplise常用快捷键
  5. FCN全卷积网络随笔
  6. 三星w系列vip服务器,高端人士候机专属特权 三星W2017一张行走的VIP卡
  7. 黑色全屏个人主页bootstrap4模板
  8. PHP 代码简洁之道 ( PHP Clean Code)(第二部分)
  9. ROS系统MoveIt玩转双臂机器人系列(二)--生成MoveIt配置包
  10. linux centos7磁盘分区扩容,centos7 xfs文件系统的磁盘扩容
  11. Genymotion下载及安装(安卓虚拟机)
  12. [转] boost undefined reference to 'pthread_create 问题
  13. 网页切图div+css命名
  14. 网页监控之自己设计监控界面
  15. 微信小程序:经典语录大全微信小程序源码
  16. Error from chokidar (C:\): Error: EBUSY: resource busy or locked, lstat ‘D:\DumpStack.log.tmp
  17. python 画出决策边界_python 画出使用分类器得到的决策边界
  18. js设计模式--代理模式
  19. 【Linux学习】基本操作
  20. Mac 系统快速迁移记录(M1 Max)

热门文章

  1. performSelector和forwardInvocation之消息的派发和转发
  2. nginx删除图片缓存
  3. java import static作用
  4. sql中将字符串分割成数组
  5. 字符串分割成数组元素和去掉重复元素
  6. 16进制数的表示方法及转换
  7. Oracle中连接本地ORCL数据库
  8. 化工——一个走向数字化的成熟行业
  9. linux文件做软连接,Linux建立软连接和硬链接
  10. 22、Camunda 补偿事件、事务子流程、分布式事务一致性