本文共 29425 字,大约阅读时间需要 98 分钟。
目录
1.进程(Process):计算机软件程序比如QQ、lol这些程序都保存在磁盘上,用户点击运行,那么程序会被调用到内存上,cpu会一条一条的从内存上取指令,运行,这就是进程。
2.线程:当用户执行程序时,会先将该程序加载到内存中,cpu从内存中取指令执行,进入cpu,还可以分多线程执行。
3.进程的特点:进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。当用户同时执行两个程序时,即有两个进程,单核cpu会合理分配时间片,雨露均沾的快速切换执行,两个进程的地址空间是相互独立的,互不干扰。如果多核cpu,那么可以并行执行。
4.进程的状态:
5.线程的状态
6.线程转换图(简化版)
源码分析图片
编程参考的图片
7.线程与进程的关系
进程是资源分配的最小单位,线程是程序执行的最小单位。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。比如打开浏览器,开启多个窗口,是多线程还是多进程?因为一个页面卡住,不会影响其他页面,所以是多进程程序,开启新的页面就开启新的进程,如果用多线程来实现,页面之间相互影响,是不正确的。
多线程一定是并发,多进程单核cpu并发,多进程多核cpu并行,多核cpu并行真的存在吗(不确定,有待进一步证明)
8.单核cpu与多进程、多线程的效率问题
单核cpu只能并发的执行多进程与多线程,至于效率问题,这里讨论多线程程序的效率问题,程序涉及到大量的阻塞问题,比如io阻塞,速度相对于cpu运算是很低的,导致该线程会阻塞等待一段时间,利用这个时间可以处理另一个线程,当io获取之后,返回执行之前的线程。
深度好文推荐:
9.同步锁——乐观锁与悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
Java中有两种创建线程的方式。
创建线程方式一:
例如下面的代码中,在主线程main中创建了两个线程对象,先后并先后调用start()开启这两个线程,这两个线程会各自执行MyThread中的run()方法。
class MyThread extends Thread { String name; String gender; MyThread(String name,String gender){ this.name = name; this.gender = gender; } public void run(){ int i = 0; while(i<=20) { //除了主线程main,其余线程从0开始编号,currentThread()获取的是当前线程对象 System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender); i++; } }} public class CreateThread { public static void main(String[] args) { MyThread mt1 = new MyThread("malong","Male"); MyThread mt2 = new MyThread("Gaoxiao","Female"); mt1.start(); mt2.start(); System.out.println("main thread over"); }}
上面的代码执行时,有三个线程,首先是主线程main创建2个线程对象,并开启这两个线程任务,开启两个线程后主线程输出"main thread over",然后main线程结束。在开启两个线程任务后,这两个线程加入到了就绪队列等待CPU的调度执行。如下图。因为每个线程被cpu调度是随机的,执行时间也是随机的,所以即使mt1先开启任务,但mt2可能会比mt1线程先执行,也可能更先消亡。
创建线程方式二:
class MyThread implements Runnable { String name; String gender; MyThread(String name,String gender){ this.name = name; this.gender = gender; } public void run(){ int i = 0; while(i<=200) { System.out.println(Thread.currentThread().getName()+"-----"+i); i++; } }} public class CreateThread2 { public static void main(String[] args) { //创建子类对象 MyThread mt = new MyThread("malong","Male"); //创建线程对象 Thread th1 = new Thread(mt); Thread th2 = new Thread(mt); th1.start(); th2.start(); System.out.println("main thread over"); }}
这两种创建线程的方法,无疑第二种(实现Runnable接口)要好一些,因为第一种创建方法继承了Thread后就无法继承其他父类。
创建线程方式三:
new Thread(()->{ while (true) { synchronized(o) { if (MySocketTestHandler.channelMap.containsKey("0")) { Channel channel = MySocketTestHandler.channelMap.get("0"); System.out.println(channel.toString()); channel.writeAndFlush("发给柜子0的"); channel.closeFuture(); } } } }).start();
结合线程状态转换图来看:
Thread类中的方法:
currentThread()
:返回值为Thread,返回当前线程对象。isAlive()
:判断线程是否还活着。活着的概念是指是否消亡了,对于运行态、就绪态、睡眠态的线程都是活着的状态。getName()
:获取当前线程的线程名称。setName()
:设置线程名称。给线程命名还可以使用构造方法Thread(String thread_name)
或Thread(Runnable r,String thread_name)
。getPriority()
:获取线程优先级。优先级范围值为1-10(默认值为5),相邻值之间的差距对cpu调度的影响很小。一般使用3个字段MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分别表示1、5、10三个优先级,这三个优先级可较大地区分cpu的调度。setPriority()
:设置线程优先级。run()
:封装的是线程开启后要执行的任务代码。如果run()中没有任何代码,则线程不做任何事情。start()
:开启线程并让线程开始执行run()中的任务。toString()
:返回线程的名称、优先级和线程组。sleep(long millis)
:让线程睡眠多少毫秒。join(t1)
:将线程t1合并到当前线程,并等待线程t1执行完毕后才继续执行当前线程。即让t1线程强制插队到当前线程的前面并等待t1完成。yield()
:将当前正在执行的线程退让出去,以让就绪队列中的其他线程有更大的几率被cpu调度。即强制自己放弃cpu,并将自己放入就绪队列。由于自己也在就绪队列中,所以即使此刻自己放弃了cpu,下一次还是可能会立即被cpu选中调度。但毕竟给了机会给其它就绪态线程,所以其他就绪态线程被选中的几率要更大一些。Object类中的方法:
wait()
:线程进入某个线程池中并进入睡眠态。等待notify()或notifyAll()的唤醒。notify()
:从某个线程池中随机唤醒一个睡眠态的线程。notifyAll()
:唤醒某个线程池中所有的睡眠态线程。一般来说,wait()和唤醒的notify()或notifyAll()是成对出现的,否则很容易出现死锁。
那么sleep与wait有什么区别?
sleep是Thread类的方法,会让出CPU,不会导致锁行为的改变,可以在任何地方使用
wati是Object类的方法,只能在synchronized方法或synchronized块中使用,不仅让出CPU,还会释放已经占有的同步资源锁
下面看下代码来区分两者的区别
public class WaitSleepDemo_Sleep { public static void main(String[] args){ Object lock = new Object(); new Thread(() ->{ System.out.println("Thread A 正在申请锁"); synchronized (lock) { try { System.out.println("Thread A 拿到锁啦"); Thread.sleep(1000); System.out.println("Thread A 结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() ->{ System.out.println("Thread B 正在申请锁"); synchronized (lock) { try { System.out.println("Thread B 拿到锁啦"); Thread.sleep(1000); System.out.println("Thread B 结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }}
打印结果:
Thread A 正在申请锁
Thread A 拿到锁啦 Thread B 正在申请锁 Thread A 结束 Thread B 拿到锁啦 Thread B 结束可以看到Thread.sleep不会释放锁,只有等A完成了B才拿到锁
public class WaitSleepDemo_Wait { public static void main(String[] args){ Object lock = new Object(); new Thread(() ->{ System.out.println("Thread A 正在申请锁"); synchronized (lock) { try { System.out.println("Thread A 拿到锁啦"); lock.wait(1000); System.out.println("Thread A 结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() ->{ System.out.println("Thread B 正在申请锁"); synchronized (lock) { try { System.out.println("Thread B 拿到锁啦"); lock.wait(1000); System.out.println("Thread B 结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }}
打印结果为:
Thread A 正在申请锁
Thread A 拿到锁啦 Thread B 正在申请锁 Thread B 拿到锁啦 Thread A 结束 Thread B 结束可以看到Object.wait会释放锁,因为A还没有执行完,锁就被B获得了
java.util.concurrent.locks包中的类和它们的方法:
Lock类中:
lock()
:获取锁(互斥锁)。unlock()
:释放锁。newCondition()
:创建关联此lock对象的Condition对象。Condition类中:
await()
:和wait()一样。signal()
:和notify()一样。signalAll()
:和notifyAll()一样。线程安全问题是指多线程同时执行时,对同一资源的并发操作会导致资源数据的混乱。
例如下面是用多个线程(窗口)售票的代码。
class Ticket implements Runnable { private int num; //票的数量 Ticket(int num){ this.num = num; } //售票 public void sale() { if(num>0) { num--; System.out.println(Thread.currentThread().getName()+"-------"+remain()); } } //获取剩余票数 public int remain() { return num; } public void run(){ while(true) { sale(); } }} public class ConcurrentDemo { public static void main(String[] args) { Ticket t = new Ticket(100); //创建多个线程对象 Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); //开启多个线程使其执行任务 t1.start(); t2.start(); t3.start(); t4.start(); }}
执行结果大致如下:
以上代码的执行过程大致如下图:(仔细分析!!!)
共开启了4个线程执行任务(不考虑main主线程),每一个线程都有4个任务:
if(num>0)
;num--
;return num
;System.out.println(Thread.currentThread().getName()+"-------"+remain())
。这四个任务的共同点也是关键点在于它们都操作同一个资源Ticket对象中的num,这是多线程出现安全问题的本质,也是分析多线程执行过程的切入点。
当main线程开启t1-t4这4个线程时,它们首先进入就绪队列等待被CPU随机选中。(1).假如t1被先选中,分配的时间片执行到任务②就结束了,于是t1进入就绪队列等待被CPU随机选中,此时票数num自减后为99;(2).当t3被CPU选中时,t3所读取到的num也为99,假如t3分配到的时间片在执行到任务②也结束了,此时票数num自减后为98;(3).同理t2被选中执行到任务②结束后,num为97;(4).此时t3又被选中了,于是可以执行任务③,甚至是任务④,假设执行完任务④时间片才结束,于是t3的打印语句打印出来的num结果为97;(5).t1又被选中了,于是任务④打印出来的num也为97。
显然,上面的代码有几个问题:(1)有些票没有卖出去了但是没有记录;(2)有的票重复卖了。这就是线程安全问题。
java中解决线程安全问题的方法是使用互斥锁,也可称之为"同步"。解决思路如下:
(1).为待执行的任务设定给定一把锁,拥有相同锁对象的线程在wait()时会进入同一个线程池睡眠。
(2).线程在执行这个设了锁的任务时,首先判断锁是否空闲(即锁处于释放状态),如果空闲则去持有这把锁,只有持有这把锁的线程才能执行这个任务。即使时间片到了,它也不是释放锁,只有wait()或线程结束时才会安全地释放锁。 (3).这样一来,锁被某个线程持有时,其他线程在锁判断后就继续会线程池睡眠去了(或就绪队列)。最终导致的结果是,(设计合理的情况下)某个线程一定完整地执行完一个任务,其他线程才有机会去持有锁并执行任务。换句话说,使用同步线程,可以保证线程执行的任务具有原子性,只要某个同步任务开始执行了就一定执行结束,且不允许其他线程参与。
让线程同步的方式有两种,一种是使用synchronized(){}
代码块,一种是使用synchronized关键字修饰待保证同步的方法。
class Ticket implements Runnable { private int num; //初始化票的数量 private Object obj = new Object(); Ticket(int num){ this.num = num; } //售票 public void sale() { synchronized(obj) { //使用同步代码块封装需要保证原子性的代码 if(num>0) { num--; System.out.println(Thread.currentThread().getName()+"-------"+remain()); } } } //获取剩余票数 public int remain() { return num; } public void run(){ while(true) { sale(); } }}
class Ticket implements Runnable { private int num; //初始化票的数量 Ticket(int num){ this.num = num; } public synchronized void sale() { //使用synchronized关键字,方法变为同步方法 if(num>0) { num--; System.out.println(Thread.currentThread().getName()+"-------"+remain()); } } //获取剩余票数 public int remain() { return num; } public void run(){ while(true) { sale(); } }}
使用同步之后,if(num>0)
、num--
、return num
和print(num)
这4个任务就强制具有原子性。某个线程只要开始执行了if语句,它就一定会继续执行直到执行完print(num),才算完成了一整个任务。只有完成了一整个任务,线程才会释放锁(当然,也可能继续判断while(true)并进入下一个循环)。总之个人理解——使用同步线程synchronized代码块后,整个代码块成为一个整体,看作一条指令,cpu只能一次性全部执行完synchronized代码块,然后被其他线程调用。
前面的示例中,同步代码块synchronized(obj){}中传递了一个obj的Object对象,这个obj可以是任意一个对象的引用,这些引用传递给代码块的作用是为了标识这个同步任务所属的锁。
使用相同的锁之间会互斥,但不同锁之间则没有任何影响。因此,要保证任务同步(原子性),这些任务所关联的锁必须相同。也因此,如果有多个同步任务(各自保证自己的同步性),就一定不能都使用同步函数。
例如下面的例子中,写了两个相同的sale()方法,并且使用了flag标记让不同线程能执行这两个同步任务。如果出现了多线程安全问题,则表明synchronized函数和同步代码块使用的是不同对象锁。如果将同步代码块中的对象改为this后不出现多线程安全问题,则表明同步函数使用的是this对象。如果为sale2()加上静态修饰static,则将obj替换为"Ticket.class"来测试。
class Ticket implements Runnable { private int num; //初始化票的数量 boolean flag = true; private Object obj = new Object(); Ticket(int num){ this.num = num; } //售票 public void sale1() { synchronized(obj) { //使用的是obj标识锁 if(num>0) { num--; try{Thread.sleep(1);} catch (InterruptedException i){} //为了确保num--和println()分开,加上sleep System.out.println(Thread.currentThread().getName()+"===sale1==="+remain()); } } } public synchronized void sale2() { //使用this标识锁 if(num>0) { num--; try{Thread.sleep(1);} catch (InterruptedException i){} System.out.println(Thread.currentThread().getName()+"===sale2==========="+remain()); } } //获取剩余票数 public int remain() { return num; } public void run(){ if(flag){ while(true) { sale1(); } } else { while(true) { sale2(); } } }} public class Mytest { public static void main(String[] args) { Ticket t = new Ticket(200); //创建多个线程对象 Thread t1 = new Thread(t); Thread t2 = new Thread(t); //开启多个线程使其执行任务 t1.start(); try{Thread.sleep(1);} catch (InterruptedException i){} t.flag = false; t2.start(); }}
以下是执行结果中的一小片段,出现了多线程安全问题。而如果将同步代码块中的obj改为this,则不会出现多线程安全问题。
Thread-0===sale1===197
Thread-1===sale2===========197
Thread-0===sale1===195
Thread-1===sale2===========195
Thread-1===sale2===========193
Thread-0===sale1===193
Thread-0===sale1===191
Thread-1===sale2===========191
1.使用(this)。一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。我们看下面一个例子
/** * 同步线程 */class SyncThread implements Runnable { private static int count; public SyncThread() { count = 0; } public void run() { synchronized(this) { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public int getCount() { return count; }}SyncThread syncThread = new SyncThread();Thread thread1 = new Thread(syncThread, "SyncThread1");Thread thread2 = new Thread(syncThread, "SyncThread2");thread1.start();thread2.start();
结果:
解释:当两个并发线程(thread1和thread2)访问同一个对象synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
我们改一下调用的主函数
结果如下:
解释:两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
2.当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块
class Counter implements Runnable{ private int count; public Counter() { count = 0; } public void countAdd() { synchronized(this) { for (int i = 0; i < 5; i ++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized public void printCount() { for (int i = 0; i < 5; i ++) { try { System.out.println(Thread.currentThread().getName() + " count:" + count); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public void run() { String threadName = Thread.currentThread().getName(); if (threadName.equals("A")) { countAdd(); } else if (threadName.equals("B")) { printCount(); } }//调用代码Counter counter = new Counter();Thread thread1 = new Thread(counter, "A");Thread thread2 = new Thread(counter, "B");thread1.start();thread2.start();}
结果:
解释:上面代码中countAdd是一个synchronized的,printCount是非synchronized的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
public synchronized void run() { for (int i = 0; i < 5; i ++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }}
public synchronized void method(){ // todo}
等效于下面代码
public void method(){ synchronized(this) { // todo }}
在用synchronized修饰方法时要注意:
synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下: 在子类方法中加上synchronized关键字class Parent { public synchronized void method() { }}class Child extends Parent { public synchronized void method() { }}
或
class Parent { public synchronized void method() { }}class Child extends Parent { public void method() { super.method(); }}
我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。我们对Demo1进行一些修改如下:
/** * 同步线程 */class SyncThread implements Runnable { private static int count; public SyncThread() { count = 0; } public synchronized static void method() { for (int i = 0; i < 5; i ++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void run() { method(); }}//调用代码SyncThread syncThread1 = new SyncThread();SyncThread syncThread2 = new SyncThread();Thread thread1 = new Thread(syncThread1, "SyncThread1");Thread thread2 = new Thread(syncThread2, "SyncThread2");thread1.start();thread2.start();
结果:
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。
Synchronized还可作用于一个类,用法如下:
class ClassName { public void method() { synchronized(ClassName.class) { // todo } }}
代码:
/** * 同步线程 */class SyncThread implements Runnable { private static int count; public SyncThread() { count = 0; } public static void method() { synchronized(SyncThread.class) { for (int i = 0; i < 5; i ++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public synchronized void run() { method(); }}
结果:synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。
/** * 银行账户类 */class Account { String name; float amount; public Account(String name, float amount) { this.name = name; this.amount = amount; } //存钱 public void deposit(float amt) { amount += amt; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } //取钱 public void withdraw(float amt) { amount -= amt; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } public float getBalance() { return amount; }}/** * 账户操作类 */class AccountOperator implements Runnable{ private Account account; public AccountOperator(Account account) { this.account = account; } public void run() { synchronized (account) { account.deposit(500); account.withdraw(500); System.out.println(Thread.currentThread().getName() + ":" + account.getBalance()); } }}//调用代码Account account = new Account("zhang san", 10000.0f);AccountOperator accountOperator = new AccountOperator(account);final int THREAD_NUM = 5;Thread threads[] = new Thread[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i ++) { threads[i] = new Thread(accountOperator, "Thread" + i); threads[i].start();}
结果:
解释:
在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。public void method3(SomeObject obj){ //obj 锁定的对象 synchronized(obj) { // todo }}
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
class Test implements Runnable{ private byte[] lock = new byte[0]; // 特殊的instance变量 public void method() { synchronized(lock) { // todo 同步代码块 } } public void run() { }}
说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
通俗易懂的解释:怎么实现两个线程之间的通知事件,而非一个线程轮询两一个线程的耗费性能操作,可以使用Wait/Notify两个方法,
synchronized (Lock.class){}可以保证代码的原子性,但是如果代码中有等待另一个线程数据的的操作,需要暂时处于等待状态,把锁丢出去,找到另一个synchronized (Lock.class){},这是第二段代码可以获得锁,执行里面的代码,同时保证了代码的原子性,并且第二段代码处理得到了第一段代码想要的数据后,就把锁释放出去,等待第二段代码原子性执行完后,就执行第一段代码,原子性也执行结束。
附代码——idea多线程调试方便。
import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;public class ThreadTest { static final Object obj = new Object(); private static boolean flag = false; public static void main(String[] args) throws Exception { Thread consume = new Thread(new Consume(), "Consume"); Thread produce = new Thread(new Produce(), "Produce"); consume.start(); Thread.sleep(1000); produce.start(); try { produce.join(); consume.join(); } catch (InterruptedException e) { e.printStackTrace(); } } // 生产者线程 static class Produce implements Runnable { @Override public void run() { synchronized (Lock.class) { System.out.println("进入生产者线程"); System.out.println("生产"); try { TimeUnit.MILLISECONDS.sleep(2000); //模拟生产过程 flag = true; Lock.class.notify(); //通知消费者 TimeUnit.MILLISECONDS.sleep(1000); //模拟其他耗时操作 System.out.println("退出生产者线程"); } catch (InterruptedException e) { e.printStackTrace(); } } } } //消费者线程 static class Consume implements Runnable { @Override public void run() { synchronized (Lock.class) { System.out.println("进入消费者线程"); System.out.println("wait flag 1:" + flag); try { System.out.println("还没生产,进入等待"); Lock.class.wait(); System.out.println("结束等待"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("wait flag 2:" + flag); System.out.println("消费"); System.out.println("退出消费者线程"); } } }}
切换查看变量。
最典型的死锁是僵局问题,A等B,B等A,谁都不释放,造成僵局,最后两个线程都无法执行下去。
例如下面的代码示例,sale1()中,obj锁需要持有this锁才能完成任务整体,而sale2()中,this锁需要持有obj锁才能完成任务整体。当两个线程都开始执行任务后,就开始产生死锁问题。
class Ticket implements Runnable { private int num; boolean flag = true; private Object obj = new Object(); Ticket(int num){ this.num = num; } public void sale1() { synchronized(obj) { //obj锁 sale2(); //this锁 } } public synchronized void sale2() { //this锁 synchronized(obj){ //obj锁 if(num>0) { num--; try{Thread.sleep(1);} catch (InterruptedException i){} System.out.println(Thread.currentThread().getName()+"========="+remain()); } } } //获取剩余票数 public int remain() { return num; } public void run(){ if(flag){ while(true) { sale1(); } } else { while(true) { sale2(); } } }} public class DeadLockDemo { public static void main(String[] args) { Ticket t = new Ticket(200); //创建多个线程对象 Thread t1 = new Thread(t); Thread t2 = new Thread(t); //开启多个线程使其执行任务 t1.start(); try{Thread.sleep(1);} catch (InterruptedException i){} t.flag = false; t2.start(); }}
为了避免死锁,尽量不要在同步中嵌套同步,因为这样很容易造成死锁。
1.重复单功能线程型问题。典型的问题是一个售票员,多个购票人。
class Ticket implements Runnable { private int num; //初始化票的数量 private Object obj = new Object(); Ticket(int num){ this.num = num; } //售票 public void sale() { synchronized(obj) { //使用同步代码块封装需要保证原子性的代码 if(num>0) { num--; System.out.println(Thread.currentThread().getName()+"-------"+remain()); } } } //获取剩余票数 public int remain() { return num; } public void run(){ while(true) { sale(); } }}
2.多功能线程型问题。典型的问题是多个蛋糕店一起做蛋糕,多个人一起吃蛋糕。
写法1(不推荐):
public class ProducersAndConsumersDemo { public static void main(String[] args){ Cake cake = new Cake(); Object lock = new Object(); CakeConsumer cakeConsumer = new CakeConsumer(cake,lock); CakeProducer cakeProducer = new CakeProducer(cake,lock); Thread threadConsumer1 = new Thread(cakeConsumer,"小明"); Thread threadConsumer2 = new Thread(cakeConsumer,"小红"); Thread threadProducer1 = new Thread(cakeProducer,"商家AAA"); Thread threadProducer2 = new Thread(cakeProducer,"商家BBB"); threadConsumer1.start(); threadConsumer2.start(); threadProducer1.start(); threadProducer2.start(); while (Thread.currentThread().isAlive()); System.out.println("主线程结束!"); }}class CakeProducer implements Runnable { Cake cake; Object lock; public CakeProducer(Cake cake,Object lock) { this.cake = cake; this.lock = lock; } public void producerCake() { cake.makeCake(); System.out.println(Thread.currentThread().getName()+"生产了一个蛋糕"); System.out.println("蛋糕总数为"+cake.num); } @Override public void run() { while (true) { try { synchronized (lock) { producerCake(); } Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}class CakeConsumer implements Runnable { Cake cake; Object lock; public CakeConsumer(Cake cake,Object lock) { this.cake = cake; this.lock = lock; } public void consumerCake() { if (haveCake()) { cake.eatCake(); System.out.println(Thread.currentThread().getName()+"吃了一个蛋糕"); System.out.println("蛋糕还剩" + cake.num); } } public boolean haveCake() { if (cake.num == 0) { System.out.println("没有蛋糕了,吃不了"); return false; } else { return true; } } @Override public void run() { while (true) { try { synchronized (lock) { consumerCake(); } Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }} class Cake { public Integer num; public Cake() { this.num = 0; } public void eatCake() { num--; } public void makeCake() { num++; }}
打印结果:
没有蛋糕了,吃不了
商家BBB生产了一个蛋糕 蛋糕总数为1 商家AAA生产了一个蛋糕 蛋糕总数为2 小红吃了一个蛋糕 蛋糕还剩1 商家AAA生产了一个蛋糕 蛋糕总数为2 小红吃了一个蛋糕 蛋糕还剩1 小明吃了一个蛋糕 蛋糕还剩0
写法2(推荐):
public class ProducersAndConsumersDemo2 { public static void main(String[] args){ Object lock = new Object(); Cake cake = new Cake(); ProducerCake producerCake = new ProducerCake(cake); ConsumerCake cakeConsumer = new ConsumerCake(cake); new Thread(() ->{ while (true) { try { synchronized (lock) { producerCake.producerCake(); } Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(() ->{ while (true) { try { synchronized (lock) { cakeConsumer.consumerCake(); } Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }}class ProducerCake { Cake cake; public ProducerCake(Cake cake) { this.cake = cake; } public void producerCake() { cake.makeCake(); System.out.println(Thread.currentThread().getName()+"生产了一个蛋糕"); System.out.println("蛋糕总数为"+cake.num); }} class ConsumerCake { Cake cake; public ConsumerCake(Cake cake) { this.cake = cake; } public void consumerCake() { if (haveCake()) { cake.eatCake(); System.out.println(Thread.currentThread().getName()+"吃了一个蛋糕"); System.out.println("蛋糕还剩" + cake.num); } } public boolean haveCake() { if (cake.num == 0) { System.out.println("没有蛋糕了,吃不了"); return false; } else { return true; } }}
打印结果:
Thread-0生产了一个蛋糕
蛋糕总数为1 Thread-1吃了一个蛋糕 蛋糕还剩0 Thread-0生产了一个蛋糕 蛋糕总数为1 Thread-1吃了一个蛋糕 蛋糕还剩0 Thread-0生产了一个蛋糕 蛋糕总数为1