明确一点:多线程不是为了提高程序执行速度(性能甚至更低),而是提高应用程序的使用效率。
多线程的三大特性:原子性、可见性、有序性
一、创建线程
创建线程额的开销:分配内存 --> 列入调度 --> 线程切换的时候还要执行内存换页,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{
@Override
public 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{
@Override
public 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;
@Override
public 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;
@Override
public 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{
@Override
public 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()的区别:
-
状态。sleep()使当前线程进入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
-
时间。sleep 方法使当前运行中的线程睡眠一段时间,进入阻塞状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
-
优先级。sleep()方法在给其他线程运行机会时不考虑线程的优先级,低优先级仍有机会被调度。而yield()方法只会给相同优先级或更高优先级的线程运行的机会。如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
4. 等待线程终止:join()
join()的作用是:“等待该线程终止”。待该线程执行完成后,才能执行其他线程。否则其余线程只能阻塞。这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。可以理解为插队!和sleep一样,也会抛出InterruptedException异常。
package com.yang;
public class Join implements Runnable {
@Override
public 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;
}
@Override
public 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{
@Override
public 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-->1
Process finished with exit code 0
但是要注意,优先级低只意味着获得调度的概率低!并不是一定按着优先级的高低来调用的!这都是看CPU的调度。
main-->5
Thread-0-->5
Thread-2-->6
Thread-1-->1
Process 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();
}
@Override
public 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();
}
@Override
public 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();
}
@Override
public 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();
}
@Override
public 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();
@Override
public 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();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
结果:
转载:https://blog.csdn.net/Shmily_0/article/details/113811145