并发和并行是即类似又有区别:(微观)java
并行:指两个或多个事件在同一时刻发生; 强调的是时间点.程序员
并发:指两个或多个事件在同一时间段内发生; 强调的是时间段.windows
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间能够影响的,又称为轻型进程或进程元。 由于一个进程中的多个线程是并发运行的,那么从微观角度上考虑也是有前后顺序的,那么哪一个线程执行彻底取决于CPU调度器,程序员是控制不了的。咱们能够把多线程并发性看做是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,这也造就了多线程的随机性。安全
建立进程的方式有两种,我以windows上的记事本为例:网络
package com.StadyJava.day14; import java.io.IOException; public class ProcessDemo { public static void main(String[] args) throws Exception { //方法1:使用Runtime Runtime runtime=Runtime.getRuntime(); runtime.exec("notepad"); //方法2:ProcessBuild ProcessBuilder processBuilder=new ProcessBuilder("notepad"); processBuilder.start(); } }
运行代码,此时会生成两个记事本。多线程
我也尝试过启动其余的程序,可是计算器不认识,只有notepad这种计算机自带的才认识。并发
若是想要启动其余的程序,只能写上绝对路径app
ProcessBuilder processBuilder=new ProcessBuilder("E:\\shuyunquan\\TIM\\Bin\\TIM.exe"); processBuilder.start();
这样我测试了,是能够的,可是没意思,这样你写了程序发布了也搞怪不了,别人电脑上的路径和你不同。。。ide
接下来说解线程。性能
下图是线程的一些经常使用的方法
建立进程的两个方法已经知道了,接下来看看建立线程的两个方法
package com.StadyJava.day14Thread; import java.lang.Thread; class Music extends Thread{ public void run() { for (int i = 0; i < 50; i++) { System.out.println("听音乐"+i); } } } public class MusicThread { public static void main(String[] args) { for (int i = 0; i < 50; i++) { System.out.println("打游戏"+i); if (i == 10) { Music music=new Music(); music.start(); } } } }
注意,继承了Thread类以后要重写run方法,并且在调用的时候,只能使用start方法,不能调用run方法,切记!
输出的结果是在打游戏10以后,下面的打游戏和听音乐都是随机出现,由于主线程main和线程music在抢占资源,谁抢到谁执行,因此输出的结果是随机的
(注意!我使用Idea输出的结果是顺序的,不是随机的。我同窗和我同样的代码使用Eclipse结果是随机的,我本身复制到Eclipse以后偶尔随机,Idea是死都不随机,这个我解决不了,但愿之后知道缘由或者有人告诉我)
其实Thread类也是实现了Runnable接口的,因此这个方法我感受是本源??
package com.StadyJava.day14Thread; import java.lang.Runnable; class MusicRun implements Runnable{ public void run() { for (int i = 0; i < 50; i++) { System.out.println("听音乐"+i); } } } public class MusicRunnable { public static void main(String[] args) { for (int i = 0; i < 50; i++) { System.out.println("打游戏"+i); if (i == 10) { Runnable music=new MusicRun(); Thread thread=new Thread(music); thread.start(); } } } }
没什么特别的,就是生成一个runnable的对象,而后Thread对象加载进这个Runnable对象,最后start方法调用一下就完事了。这个结果个人Idea依然是显示的不符合个人指望的。
上面两种建立线程的方式是最经常使用的方式,通常也就足够了,下面介绍一下不怎么经常使用的
匿名内部类的格式:new 接口(){} 应该是这样的,待补充
其实匿名内部类建立线程仍是使用上面的两种方式,只不过那个Music类我不须要去定义了,这就是匿名内部类,看代码吧
package com.StadyJava.day14Thread; import java.lang.Runnable; class MusicRun implements Runnable{ public void run() { for (int i = 0; i < 50; i++) { System.out.println("听音乐"+i); } } } public class MusicRunnable { public static void main(String[] args) { for (int i = 0; i < 50; i++) { System.out.println("打游戏"+i); if (i == 10) { //匿名内部类的形式1,使用接口 new Thread(new Runnable() { public void run() { for (int i = 0; i < 50; i++) { System.out.println("听音乐"+i); } } }).start(); //匿名内部类的形式2,使用继承类 new Thread(){ public void run() { for (int i = 0; i < 50; i++) { System.out.println("听音乐"+i); } } }.start(); } } } }
这回,输出的结果总算是符合个人预期了,多是线程变成了3个,抢占资源激烈了些。。。
学了两种常见的建立线程的方法以后,他们之间有什么区别呢?
我写个例子,吃苹果大赛,3我的参加比赛,先使用继承Thread类建立线程的方式,代码以下:
package com.StadyJava.day14; class Person extends java.lang.Thread{ private int num=50; public Person(String name){ super(name); } public void run() { for (int i = 0; i < 50; i++) { if (num >0) { System.out.println(super.getName()+"吃了编号为"+num--+"的苹果"); } } } } public class EatAppleThread { public static void main(String[] args) { //建立3我的,去参加吃苹果大赛 new Person("许嵩").start(); new Person("林俊杰").start(); new Person("蜀云泉").start(); } }
这个输出的结果,就是许嵩,林俊杰,蜀云泉每一个人都吃了50 个🍎。缘由是由于我new了三个对象,没个对象都有num变量,他们之间互不干扰,以下图所示:
这样很很差,个人吃苹果大赛是总共50个🍎,大家3我的来吃就完事了。咱们看看实现Runnable接口建立线程的方式是怎么样的,代码以下:
package com.StadyJava.day14; class Apple implements Runnable{ private int num=50; public void run() { for (int i = 0; i < 50; i++) { if (num >0) { //返回当前线程的引用Thread.currentThread(),再获取名字 System.out.println(Thread.currentThread().getName()+"吃了编号为"+num--+"的苹果"); } } } } public class EatAppleRunnable { public static void main(String[] args) { //建立3我的,去参加吃苹果大赛 Apple apple=new Apple(); new Thread(apple,"许嵩").start(); new Thread(apple,"林俊杰").start(); new Thread(apple,"蜀云泉").start(); } }
此次由于我传入的都是apple这个对象,这一个对象有50个🍎,建立了3个线程,此次输出的结果是OK的,缘由以下图所示,3个线程共用了一个苹果对象。
从这个吃苹果比赛的例子中能够总结一下继承Thread类建立线程的方式和实现Runnable接口建立线程的方式的区别:
综合上面的区别对比,咱们的这个比赛。看来只能使用实现Runnable接口建立线程的方式来实现了。推荐之后建立线程,都使用实现Runnable接口的方式。
拿上面写的实现接口建立线程的吃苹果比赛为例,这个是存在线程安全的,咱们能够写一个线程休眠来看看,这样更容易观察。
须要说明,Thread.sleep线程休眠,须要使用try catch来扑捉异常,不能使用throw抛出,由于Runnable接口里面的run方法自己就没有throw的写法
package com.day14; class Apple implements Runnable{ private int num=50; public void run() { for (int i = 0; i < 50; i++) { if (num >0) { //线程休眠,必须使用try catch扑捉异常 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //返回当前线程的引用Thread.currentThread(),再获取名字 System.out.println(Thread.currentThread().getName()+"吃了编号为"+num--+"的苹果"); } } } } public class EatAppleRunnable { public static void main(String[] args) { //建立3我的,去参加吃苹果大赛 Apple apple=new Apple(); new Thread(apple,"许嵩").start(); new Thread(apple,"林俊杰").start(); new Thread(apple,"蜀云泉").start(); } }
我就加了一个线程休眠,咱们如今来看看输出的结果是什么样的
竟然有0,还有-1 不是已经写了if(num>0)吗?出现这种状况的缘由是,许嵩,林俊杰,蜀云泉这三个线程都在num>0的时候,例如num=1的时候,他们仨都拿到了资源,均可以去执行run方法
这个时候就会出现这种状况。通俗一点讲许嵩拿到了最后一个苹果,可是没吃,林俊杰和蜀云泉来抢。因为这个苹果不具有独占性,因此最后一个苹果被许嵩,林俊杰,蜀云泉都咬了一口。他们都宣称本身吃了苹果,因此就会出现0和-1的状况。这样显然是不容许的。这就是
多线程并发的访问一个资源产生的安全问题
要想解决这个问题,就必需要保证苹果的数量减小必须保证同步,许嵩拿了最后一个苹果,这个时候苹果数量同步为0,剩下的人不能再抢了。
许嵩这个线程在操做的时候,林俊杰和蜀云泉只能等着。等许嵩操做完了。许嵩,林俊杰和蜀云泉才有机会去从新抢资源。
意思是这样,方法有3种
方法1.同步代码块
方法2.同步方法
方法3.锁机制(Lock)
语法:
synchronized(同步锁){
须要同步操做的代码
}
同步锁:为了保证每一个线程都能单独的执行操做,java线程同步的机制。同步锁也叫
同步监听对象/同步锁/同步监听器/互斥锁
这些都是别名,就像茴香豆的“茴”字有几种写法同样,都是别名。
Java程序中的任何对象均可以做为同步监听对象,可是咱们通常把多个线程同时访问的共享资源做为同步监听对象。监听其它的单独的对象有啥意义。注意,在任什么时候候,最多运行一个线程拥有同步锁。
代码以下:
package com.day14; class Apple implements Runnable{ private int num=50; public void run() { for (int i = 0; i < 50; i++) { //方法1:同步代码块 //因为我多个线程共享的是Apple对象,因此同步锁就是this,当前类的对象。不能使用num变量,由于num变量一直在变化 synchronized (this) { if (num > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //返回当前线程的引用Thread.currentThread(),再获取名字 System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果"); } } } } } public class EatAppleRunnable { public static void main(String[] args) { //建立3我的,去参加吃苹果大赛 Apple apple = new Apple(); new Thread(apple, "许嵩").start(); new Thread(apple, "林俊杰").start(); new Thread(apple, "蜀云泉").start(); } }
运行结果以下:
这下不会出现抢苹果事件了,也不会出现数量为0和-1的状况了。
synchronize修饰的方法就是同步方法,保证当前线程执行的时候,其它线程只能等待
语法:
synchronize public void Dowork(){
执行操做
}
同步锁:对于非static方法,同步锁就是this,对于静态方法,同步锁就是当前方法所在类的字节码对象(类.class)
注意!不能使用synchronize修饰run方法。
package com.day14; class Apple implements Runnable{ private int num=50; public void run() { for (int i = 0; i < 50; i++) { eat(); } } //方法2:同步方法,直接用synchronized修饰方法 synchronized private void eat(){ if (num > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //返回当前线程的引用Thread.currentThread(),再获取名字 System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果"); } } } public class EatAppleRunnable { public static void main(String[] args) { //建立3我的,去参加吃苹果大赛 Apple apple = new Apple(); new Thread(apple, "许嵩").start(); new Thread(apple, "林俊杰").start(); new Thread(apple, "蜀云泉").start(); } }
运行结果也是OK的
或许有人会想,既然方法加了个synchronize就线程安全了,那我把全部的方法都加上synchronize不就得了。答案是不行滴
synchronize的优缺点:
优势:保证了多线程并发访问时的同步操做,避免了多线程操做的安全问题。
缺点:使用synchronize同步方法/同步代码块会致使性能下降。
锁机制用到的是Lock这个接口,固然咱们在写代码的时候使用的是Lock接口的一个实现子类,叫ReentrantLock
语法:
private final Lock lock=new ReentrantLock(); try{ 线程操做代码 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); }
仍是上面的吃苹果比赛,使用锁机制的代码以下:
package com.day14; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Apple implements Runnable{ //建立一个Lock接口的实现子类对象。ReentrantLock是Lock接口的一个子类的实现。 private final Lock lock=new ReentrantLock(); private int num=50; public void run() { for (int i = 0; i < 50; i++) { eat(); } } //方法3:同步锁(Lock)的方式,这个方式和方法2的同步方式很相似。 private void eat(){ //进入方法首先上锁 lock.lock(); if (num > 0) { try { //返回当前线程的引用Thread.currentThread(),再获取名字 System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果"); Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } finally { //结束后,记得释放锁 lock.unlock(); } } } } public class EatAppleRunnable { public static void main(String[] args) { //建立3我的,去参加吃苹果大赛 Apple apple = new Apple(); new Thread(apple, "许嵩").start(); new Thread(apple, "林俊杰").start(); new Thread(apple, "蜀云泉").start(); } }
这个锁机制是否是和synchronize同步方法很相像,只不过同步方法是synchronize修饰的方法,而锁机制是在方法里面上锁,释放锁。
锁机制和同步代码块/同步方法比,范围更普遍。也就是说锁机制包括了同步代码块和方法,并且范围更大,更加面向对象。上锁,释放锁都本身来写,还有建立实现Lock接口的对象。
如今来说一个生产者和消费者的问题,讲定生产者生产一些东西,放到分享池中,而后消费者去分享池中消费东西,大概就是下图那样的展现:
这就是咱们的生产者和消费者的模型,咱们要根据这个写代码。记得加上上面学习的同步锁知识。
分享池代码:
package com.day15; public class ShareResource { private String name; private String sex; private int num; private boolean isEmpty=true; synchronized public void push(String name,String sex,int num) { try { while (!isEmpty) {//若是不为空的时候,生产者线程就等待 this.wait(); } //开始生产,生产事后,要isEmpty变成为空,而后唤醒其它的线程 this.name=name; this.sex=sex; this.num=num; isEmpty=false; this.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized public void popup(){ try { while (isEmpty) { this.wait(); } System.out.println(this.name + this.sex + this.num); isEmpty=true; this.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } }
生产者代码:
package com.day15; //生产者线程 public class Producer implements Runnable{ //分享池对象 public ShareResource shareResource=null; public Producer(ShareResource shareResource) { this.shareResource=shareResource; } @Override synchronized public void run() { for (int i = 0; i <50 ; i++) { if (i % 2==0) { shareResource.push("许嵩","男",i); } else{ shareResource.push("梦中人","女",i); } } } }
消费者代码:
package com.day15; public class Consumer implements Runnable { //分享池对象 public ShareResource shareResource=null; public Consumer(ShareResource shareResource) { this.shareResource=shareResource; } @Override synchronized public void run() { for (int i = 0; i <50 ; i++) { shareResource.popup(); } } }
执行测试代码:
package com.day15; //测试类 public class RunTest { public static void main(String[] args) { //建立生产者和消费者共同的资源对象 ShareResource resource=new ShareResource(); //启动生产者线程 new Thread(new Producer(resource)).start(); new Thread(new Producer(resource)).start(); //启动消费者线程 new Thread(new Consumer(resource)).start(); new Thread(new Consumer(resource)).start(); } }
代码就是这些,我实行的是生产者生产一个东西,消费者就去消费,我这里是直接打印出来了。生产者生产以后就去wait休息,等到东西没了才开始干活。这里咱们学到了两个新的方法
1.wait()方法:线程休眠,除了被唤醒,不然就会一直睡觉休息,和睡美人是差很少了,没有人唤醒是不会苏醒的。wait方法里面能够加参数,毫秒,就是没人唤醒的话就本身醒(好惨啊...)
2.notifyAll()方法:唤醒除本身之外全部的线程,还有一个方法是notify(),唤醒随机的一个线程
最后咱们看看输出的结果:
经过上面同步方法synchronized和线程的wait方法,notify方法很好的完成了生产者和消费者的问题。这里须要说明的是,wait方法和notify方法都必须须要同步锁,那么,我如今想用Lock锁机制去完成生产者消费者问题,那该怎么办呢?
锁机制Lock是没法使用wait和notify方法的,那使用锁机制的线程之间怎么进行通讯呢?
从Java5开始就为锁机制的线程提供了Condition接口,用于线程直接的通讯,主要使用的方法有:
1.await()方法,至关于wait()方法,线程睡眠
2.signal()方法,至关于notify()方法,随机的唤醒任意一个线程
3.signalAll()方法,至关于notifyAll()方法,唤醒除了本身之外的全部的线程
而后代码其实就修改了分享池的代码,放出来看一下:
package com.StadyJava.day15; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ShareResource { private String name; private String sex; private int num; private boolean isEmpty=true; private final Lock lock=new ReentrantLock(); //建立lock锁机制的Condition对象 private Condition condition=lock.newCondition(); public void push(String name,String sex,int num) { lock.lock(); try { while (!isEmpty) { condition.await(); } //开始生产,生产事后,要isEmpty变成为空,而后唤醒其它的线程 this.name=name; this.sex=sex; this.num=num; isEmpty=false; condition.signalAll(); } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } synchronized public void popup(){ lock.lock(); try { while (isEmpty) { condition.await(); } System.out.println(this.name + this.sex + this.num); isEmpty=true; condition.signalAll(); } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } }
其实没什么大变化,就是方法换了名字而已。
死锁就是两个线程互相在等待对方释放锁,死锁的现象一旦出现,是解决不了的,因此死锁现象只能避免,不能解决。
关于死锁,有一个超级经典的例子,就是哲学家就餐问题
以下图所示,Java线程有6种状态,如今来介绍一下各类状态。
线程有几个很重要的方法须要讲一下
这个方法是否是和上面讲的wait方法很像?其实不同,wait睡觉以后,同步锁就释放了。sleep睡觉的时候,同步锁是牢牢的抓住不松手的
这个方法大部分用来模拟网络延迟,由于你刷新网页的时候不是会转圈圈吗,能够模拟这个。代码中也有不少地方用这个来写东西,例如我想模拟一个定时炸弹,我能够这样写代码:
package com.StadyJava.day15; public class ThreadDemo { public static void main(String[] args) { for (int i = 10; i > 0; i--) { System.out.println("离爆炸还有"+i+"秒"); } System.out.println("嘣,爆炸啦"); } }
这样写,没问题吧,有问题的,我想定时,可是这个一运行,结果全出来了,不是想要的结果,因此咱们能够加一个睡眠1秒来实现,代码以下:
package com.StadyJava.day15; public class ThreadDemo { public static void main(String[] args) throws Exception { for (int i = 10; i > 0; i--) { System.out.println("离爆炸还有"+i+"秒"); Thread.sleep(1000); } System.out.println("嘣,爆炸啦"); } }
记得,sleep方法是要抛出异常的
这样就能够了,实现了倒计时的效果。还有几个例子,例如坦克发射子弹,那这个子弹确定是不断位移的,咱们设置好以后,子弹可能瞬间就打出去了,你根本看不到子弹的运行轨迹。为了仔细的观察,或者实现子弹慢速射击的一个要求,咱们也能够去sleep一下。还有NBA投篮游戏也是,均可以去试试。
线程的join方法表示一个线程等待另外一个线程完成后才执行。就是说把当前线程和当前线程所在的线程联合成一个线程。join方法被调用以后,线程对象处于阻塞状态。
适用于A线程须要等到B线程执行完毕,再拿B线程的结果再继续运行A线程。写个代码
package com.StadyJava.day15; class Join extends Thread{ public void run() { for (int i = 0; i < 50; i++) { System.out.println("join"+i); } } } public class ThreadDemo { public static void main(String[] args) throws Exception { Join joinThread=new Join(); for (int i = 0; i < 50; i++) { System.out.println("main"+i); if (i == 1) { joinThread.start(); } else if (i == 20) { joinThread.join(); } } } }
运行结果以下:
Idea副线程想和主线程抢资源真难。。。。运行了好几回才抢到。。。咱们能够看到在主线程等于1的时候,两个线程开始抢占资源打印输出。在主线程为20的时候,joinThread线程就调用了join方法,这个时候他们俩就变成了联合线程,主线程main开始进入阻塞状态,必须等到joinThread线程执行完毕才能够执行。
在后台运行,其目的是为其余线程提供服务,也称为“守护线程。
JVM的垃圾回收器就是典型的后台线程。
特色:若全部的前台线程都死亡,后台线程自动死亡。
测试线程对象是否为后台线程:使用thread.isDaemon()。
前台线程建立的线程默认是前台线程,而且当且仅当建立线程是后台线程时,新线程才是后台线程。
设置后台线程:thread.setDaemon(true),该方法必须在start方法调用前,不然出现IllegalThreadStateException异常。
package com.StadyJava.day15; class Daemon extends Thread{ public void run() { for (int i = 0; i < 50; i++) { System.out.println(super.getName()+"-"+super.isDaemon()+i); } } } public class ThreadDemo { public static void main(String[] args) throws Exception { for (int i = 0; i < 50; i++) { System.out.println("main"+i); if (i == 1) { Daemon daemon=new Daemon(); daemon.setDaemon(true);//设置线程为后台线程,必须先设置后台线程才能start开启,先start开启再设置会报错 daemon.start(); Thread.sleep(10); } } } }
就一个判断是不是后台线程的方法 isDaemon() 和一个设置线程为后台线程的方法 setDaemon(true)
若是没有了前台线程,后台线程会死亡。
优先级有两个方法
1.setPriority() 获取当前线程的优先级,main线程的优先级是5,默认的都是5
2.setPriority() 设置线程的优先级,不一样的操做系统是不同的,Linux和Windows都不同。可是有3个数字是统一的,分别是1,5,10 1是最低,10是最高。因此用这3个数字就能够了。
注意:并非优先级高的线程必定先执行,而是说这个线程有更多的机会去执行。执行的概率大了一些。
下面看一个代码
package com.StadyJava.day15; class PriorityThread extends Thread{ public PriorityThread (String name){ super(name); } public void run() { for (int i = 0; i < 50; i++) { System.out.println("我是"+getName()+i); } } } public class Priority { public static void main(String[] args) { PriorityThread priorityThread1=new PriorityThread("优先级高的线程"); PriorityThread priorityThread2=new PriorityThread("优先级低的线程"); //priorityThread1.setPriority(Thread.MIN_PRIORITY);也可使用这个,可是数字更简单 priorityThread1.setPriority(10); priorityThread2.setPriority(5); priorityThread2.start(); priorityThread1.start(); } }
看结果也知道,优先级高并非必定先执行。
古有孔融让梨,那么线程也有一个礼让的方法叫 yield方法。这个方法比较特别,他把本身执行的机会让给那些优先级高的线程,提出这个请求给调度器CPU,可是CPU能够赞成这个请求也能够无视这个请求
这个yield方法和sleep方法的区别以下:
1.均可以使得当前处于运行状态的线程放弃执行的机会,让给其它线程
2.sleep方法会让给其它线程,随机的让。yield方法会让给那些优先级高的线程。
3.调用sleep方法后,线程会进入计时等待状态。调用yield方法后,线程会进入就绪状态。
这个yield方法通常是不用的。不使用。。。。。在调试和测试线程的时候,可能会重现多线程的错误,可能。。。。因此仍是了解一下就好吧
定时器,就是定时去执行啦,直接看代码
package com.StadyJava.day15; import java.util.Timer; import java.util.TimerTask; class Vae extends TimerTask { public void run() { System.out.println("你们好,我是Vae"); } } public class TimerDemo { public static void main(String[] args) { new Timer().schedule(new Vae(),3000,1000); } }
注意,个人Vae类是继承的TimeTask类,不是Thread类。定时器的方法就是schedule方法,第一个参数就是TimeTask对象,第二个参数就是第几秒出现执行,第三个参数就是间隔多少秒执行一次。
线程组,就是多个相同的线程在一个组里面,就像老师讲课,给A同窗讲一遍,再给B同窗讲一遍,再给C同窗讲一遍。。。。这样太麻烦。直接让ABC三个同窗都过来,一块儿听就完事了。这就是线程组的意义
线程组的特色:
1.若是线程A建立了线程B,那么B和A必定是一组的。
2.一个线程一旦加入了线程组,一生就是这个组的,一天是不良人,一生都是不良人。
当Java程序运行时,JVM会建立一个main线程组,默认全部的线程都是main线程组的
synchronized是一个修饰符,Lock是一个类。这是最本质的区别。
简单的说,因为wait,notify和notifyAll都是锁级别的操做,因此把他们定义在Object类中,由于锁属于对象。