多线程
单核CPU使用的是时间片轮换来实现多线程的,多核CPU才是真正意义上的多线程
并行与并发
- 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
- 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
在通常执行的Main方法中,会执行三个线程,主线程Main、处理异常线程、垃圾收集器线程。
创建线程的三种的方式
- 继承Thread类,重写run方法
- 实现Runnable接口,实现run方法
- 实现Callable接口,实现call方法
线程、进程
程序:程序是指令与数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
进程:是程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。通常一个进程中可以包含多个线程,至少包含一个。
线程:是CPU调度和执行的单位。
真正的多线程指多个CPU,即多核。在单核的情况下,在同一时间点CPU只能执行一个代码,但是因为切换的很快,造成了同时执行的错觉。
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
继承Thread类
- 创建Thread子类
- 重写run()方法
- 调用Thread的start方法
要继承Thread类,具有挣强资源的能力,编写的线程任务/逻辑并不是随便写一个方法,而是要重写Thread中的run()方法。
public class TestThread extends Thread{
@Override
public void run(){
for(int i = 0;i<10;i++){
System.out.println("thread"+i);
}
}
}
测试与主线程挣强资源
public class Test{
public static void main(String[] args){
//主线程1
for(int i = 0;i<10;i++){
System.out.println("main1"+i);
}
//创建其他线程与主线程挣强资源
TestThread thread = new TestThread();
//thread.run(); //run方法不能直接调用
//要启动启动线程
thread.start();
//主线程2
for(int i = 0;i<10;i++){
System.out.println("main2"+i);
}
}
}
你要执行的线程要先与主线程创建。
修改线程名称
可以使用线程父类Thread的getName()与setName()方法。
public class TestThread extends Thread{
@Override
public void run(){
for(int i = 0;i<10;i++){
System.out.println(super.getName()+i);
}
}
}
可以通过Thread.currentThread()获取当前线程
public class Test{
public static void main(String[] args){
//主线程1
Thread.currentThread().setName("主线程");
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThreaad().getName()+"1"+i);
}
//创建其他线程与主线程挣强资源
TestThread thread = new TestThread();
//TestThread thread = new TestThread("子线程1");
thread.setName("子线程1");
//thread.run(); //run方法不能直接调用
//要启动启动线程
thread.start();
//主线程2
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThreaad().getName()+"2"+i);
}
}
}
也可以使用构造器设置线程的名字。
public class TestThread extends Thread{
public TestThread(String name){
super(name);
}
public TestThread(){}
@Override
public void run(){
for(int i = 0;i<10;i++){
System.out.println(super.getName()+i);
}
}
}
练习-买火车票
三个窗口,每个窗口100个人强10张票。
创建买票线程
public class BuyTicketThread extends Thread{
private static int ticketNum = 10;
public TestThread(String name){
super(name);
}
public TestThread(){}
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
//可以在这里制造延时
if(ticketNum>0){
System.out.println("从第"+super.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
}
因为三个线程共用一个“票”资源,所以要将ticketNum设为静态的。
public class Test{
public static void main(String[] args){
//1号窗口
BuyTicketThread thread1 = new BuyTicketThread("1");
thread1.start();
//2号窗口
BuyTicketThread thread2 = new BuyTicketThread("2");
thread2.start();
//3号窗口
BuyTicketThread thread3 = new BuyTicketThread("3");
thread3.start();
}
}
运行时会出现错误
实现Runnable接口
- 实现Runnable接口
- 实现run方法
- 创建实现类对象,注入Thread对象中,使用Thread对象的start方法启动
不同于继承Thread类要重写run()方法,实现Runnable接口要实现run()方法。
public class TestThread implements Runnable{
public TestThread(String name){
super(name);
}
public TestThread(){}
@Override
public void run(){
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
主线程
public class Test{
public static void main(String[] args){
//创建其他线程与主线程挣强资源
TestThread thread = new TestThread();
Thread tt = new Thread(thread,"子线程");
thread.start();
//主线程
for(int i = 0;i<10;i++){//主线程名称默认为main
System.out.println(Thread.currentThreaad().getName()+"2"+i);
}
}
}
练习-买火车票
三个窗口,每个窗口100个人强10张票。
创建买票类
public class BuyTicketThread implements Runnable{
public int ticketNum = 10;
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
if(ticketNum>0){
System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
}
public class Test{
public static void main(String[] args){
//创建线程任务
BuyTicketThread thread = new BuyTicketThread();
//1号窗口
Thread t1 = new Thread(thread,"1");
t1.start();
//2号窗口
Thread t2 = new Thread(thread,"2");
t2.start();
//3号窗口
Thread t3 = new Thread(thread,"3");
t3.start();
}
}
可以看到,由于调用的是同一Runnable实现类,线程中的公用属性“票”并不用设置为静态值。
问题:运行结果中还是会出现不同窗口买到同一张票的问题,这需要线程同步来解决。
龟兔赛跑
public class Race implements Runnable{
private static String winner;
@Override
public run(){
for(int i=1;i<=100;i++){
if("兔子".equals(Thread.currentThread.getName())){
if(i==51){//50米睡30秒
sleep(15000);
}
run(100);//兔子要100毫秒跑一米
}else{
run(200);//乌龟要200毫秒跑一米
}
}
if(!gameOver()){//两个都跑完,两个以上个参赛者不能这样写
System.put.println("winner is"+Thread.currentThread.getName());
}
}
private boolean gameOver(){
if(winner==null){
winner = Thread.currentThread().getName();
return true;
}
return false;
}
//睡m毫秒
private void sleep(long m){
try{
System.put.println(Thread.currentThread.getName()+"开始睡觉");
Thread.sleep(m);//兔子要睡m毫秒
System.put.println(Thread.currentThread.getName()+"睡醒了");
}catch(Exception ex){
ex.printStackTrace();
}
}
//跑m毫秒
private void run(long m){
try{
Thread.sleep(m);
System.put.println(Thread.currentThread.getName()+"---->跑了"+i+"米");
}catch(Exception ex){
ex.printStackTrace();
}
}
public static void main(String[] args){
Race r = new Race();
Thread t1 = new Thread(r,"兔子");
Thread t2 = new Thread(r,"乌龟");
t1.start();
t2.start();
}
}
实现Callable接口
前两种方法,无法设置返回值也无法抛出异常。Callable是一个泛型接口。
- 实现Callable接口如果不带泛型,call()的返回值就为Object
- 如果带泛型,那么call的返回值就是对应的泛型
- call方法有返回值,可以抛出异常
实现方法
- 继承Callable接口
- 实现call方法
- 开启线程
开启线程有很多方法,我们这里给出两种,一种是用FutureTask,一种使用ExecuterService
测试随机数
public class TestRandomNum implements Callable<Integer>{
@Override
public Integer call() throw Exception{
return new Random().nextInt(10);
}
}
class Test{
public static void main(String[] args){
//方法一
TestRandomNum trn1 = new TestRandomNum();
FutureTask tf = new FutureTask(trn1);
Thread tt = new Thread(tf);
tt.start();
//返回值要通过FutureTask获取
Integer o = (Integer)tf.get();
System.out.println("方式一"+o);
System.out.println("==================================");
//方法二
TestRandomNum trn2 = new TestRandomNum();
//创建执行服务
ExecuterService ser = Executors.newFixedThreadPool(2);
//提交执行
Future<Integer> r1 = ser.submit(trn1);
Future<Integer> r2 = ser.submit(trn2);
//获取结果
try{
Integer i = r1.get();System.out.println("方式二 trn1: "+i);
Integer j = r1.get();System.out.println("方式二 trn2: "+j);
}catch(Exception ex){
ex.printStackTrace();
}
//关闭服务
ser.shutdowNow();
}
}
静态代理
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。简单来说就是:使用一个代理对象将对象包装起来,然后用该代理对象来取代该对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时调用原始对象的方法,也就是为其他对象提供一种代理以控制对这个对象的访问。
- 真实对象与代理对象要同时实现一个接口
- 代理对象要代理真实角色
public class StaticProxy{
public static void main(String[] args){
You y = new You();
WeddingCompany w = new WeddingCompany(y);
w.HappyMarry();
}
}
//结婚的接口
interface Marry{
void HappyMarry();
}
//结婚对象 真实角色
class You implements Marry{
@Override
public void HappyMarry(){
System.out.println("我要结婚了");
}
}
//代理角色
class WeddingCompany implements Marry{
//真是目标角色
private Marry target;
public WeddingCompany(Marry target){
this.target = target;
}
@Override
public void HappyMarry(){
before();
this.target.HappyMarry();//真实对象的任务
after();
}
private void after(){
System.out.println("收尾款");
}
private void before(){
System.out.println("布置现场");
}
}
使用了静态代理模式情况下,我们没有修改真实对象类中的业务代码,而是选择以代理类的方式增强了他的功能,耦合度低,可扩展性好。静态代理由于不需要反射获取目标对象,所以性能也更好。
但代理模式会造成系统设计中类的数量增加,在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢,同时也增加了系统的复杂度。
在实现Runnable接口来实现多线程时,我们看到了实现类与Thread类都实现了Runnable接口,并且都实现了run方法,同时在开启线程时将实现类交给了Thread对象,这就是静态代理,而Thread就是静态代理类。
Lamda表达式
lambda 表达式的语法格式如下:
(parameters) -> expression
或
(parameters) ->{ statements; }
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
Lamda表达式属于函数式编程。了解Lamda表达式,要先了解函数式接口==“Functional interface”==
函数式接口的定义:
- 任何接口只要包含一个抽象方法,那么他就是函数式接口,例如Runnable接口
- 对于函数式接口我们可以通过lamda表达式来创建
public class Test{
static class Me2 implements Like{
@Override
public void ILike(){
System.out.println("me2 我喜欢");
}
}
public static void main(String[] args){
//方式一 创建实现类
Like like = new Me1().ILike();
//方式二 静态内部类
like = new Me2().ILike();
//方式三 局部内部类
class Me3 implements Like{
@Override
public void ILike(){
System.out.println("me3 我喜欢");
}
}
like = new Me3().ILike();
//方式四 匿名内部类
like = new Like() {
@Override
public void ILike(){
System.out.println("me4 我喜欢");
}
};
like.ILike();
//方法五 使用Lamda表达式
like = ()->{
System.out.println("lamda 我喜欢");
};
like.ILike();
}
}
interface Like{
void ILike();
}
class Me1 implements Like{
@Override
public void ILike(){
System.out.println("me1 我喜欢");
}
}
避免了内部类过多,可以让代码看上去更简洁。
例子
public class Test{
public static void main(String[] args){
MathOperation add = (int a, int b) -> a + d;
MathOperation sub = (a,b) -> {
return a-b;
};
System.out.println("100+32 = "+add.operation(100,32));
System.out.println("100-32 = "+sub.operation(100,32));
}
}
interface MathOperation {
int operation(int a, int b);
}
使用总结:
- Lamda表达式的使用前提必须是函数式接口
- 表达式的()在参数只有一个时可以省略,{}在只有一行代码实现时可以省略
- 表达式的()中参数类型可以省略
Runnable就是一个函数式接口。
生命周期
线程的常见方法
- start(): 启动当前线程,表面生调用start方法,实际上在调用线程中的run方法
- run(): 线程类,继承Thread与实现Runnable接口的时候,都要重新实现这个run方法,run中有线程中要执行的内容。
- currentThread(): Thread中的一个静态方法,获取当前正在执行的线程。
- setName(): 设置线程名
- getName(): 获取线程名
设置优先级
使用线程对象的setPriority与getPriority,线程优先级最小为1,最大为10,设置优先级不可以超范围,若不配置默认的优先级为5,优先级越高CPU调度的概率就越高。
若是同等级线程,实行的是先到先得。
public class TestThread01 extends Thread{
@Override
public void run(){
for(int i=1;i<=10;i++){
System.out.println(i);
}
}
}
class TestThread02 extends Thread{
@Override
public void run(){
for(int i=21;i<=30;i++){
System.out.println(i);
}
}
}
class Test{
public static void main(String[] args){
TestThread01 t1 = new TestThread01();
//设置线程t1的优先级
t1.setPriority(1);//优先级别高
t1.start();
TestThread02 t2 = new TestThread02();
t2.setPriority(10);//优先级别低
t2.start();
}
}
join
调用join方法的线程会被优先执行,其他线程阻塞,当前线程被执行完之后,其他线程才会执行,通过线程对象调用。
public class TestThread01 extends Thread{
public TestThread01(String name){
super(name);
}
@Override
public void run(){
for(int i=1;i<=10;i++){
System.out.println(this.getName()+i);
}
}
}
class Test{
public static void main(String[] args){
for(int i=1;i<=100;i++){
if(i==12){
TestThread01 t = new TestThread01("子线程1");
t.start();
t.join();//子线程1会被优先执行
}
System.out.println(Threah.currentThread().getName+i);
}
}
}
join()方法和sleep()方法的区别:
两者的区别在于:sleep(2000)不释放锁,join(2000)释放锁,因为join()方法内部使用的是wait(),因此会释放锁。看一下join(2000)的源码就知道了,join()其实和join(2000)一样,无非是join(0)而已:
sleep
人为的制造阻塞时间,是Thread下的一个静态方法,单位为毫秒级。
public class Test{
public static void main(String[] args){
try{
Thread.sleep(3000);
System.out.println(Threah.currentThread().getName);
}catch(Exception ex){
ex.printStackTrace();
}
}
}
每个对象都有一个锁,但是sleep并不会释放锁
案例:秒表
public class Test{
public static void main(String[] args){
//定义一个时间格式
DateFormat df = new SimpleDateFormat("HH:mm:ss");
Date date;
while(true){
date = new Date();
System.out.println(df.format(date));
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
}
}
}
在测试时,我们可以在程序的适当位置添加sleep来放大程序所存在的问题。
setDeamon
- 线程分为用户线程与守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如:监控内存、垃圾回收、后太记录操作日志等等
设置守护线程:
将子线程设置为主线程的伴随线程,主线程结束,伴随线程也结束。要先设置在启动。会出现垂死挣扎现象
public class TestThread extends Thread{
@Override
public void run(){
for(int i=1;i<=1000;i++){
System.out.println("子线程-----"+i);
}
}
}
class Test{
public static void main(String[] args){
TestThread t = new TestThread();
t.setDeamon(true);//默认是false用户线程,会出现垂死挣扎现象
t.start();
//主线程还要输出1~10
for(int i=1;i<=10;i++){
System.out.println("main-----"+i);
}
}
}
main为用户线程,所以main必定要完成,而子线程为守护线程不必执行完。
yield
暂停当前正在执行的线程对象但是不阻塞(从运行转换为就绪,重新让CPU调度,但还可能是该线程继续执行),并执行其他线程,是Thread的一个静态方法
public class TestThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"--start");
Thread.yield();//礼让不一定成功
System.out.println(Thread.currentThread().getName()+"--stop");
}
}
class Test{
public static void main(String[] args){
TestThread tt = new TestThread();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
t1.start();
t2.start();
}
}
getState
获得线程状态
-
线程状态 Thread.State枚举类。线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。RUNNABLE
在Java虚拟机中执行的线程处于此状态。BLOCKED
被阻塞等待监视器锁定的线程处于此状态。WAITING
正在等待另一个线程执行特定动作的线程处于此状态。TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。TERMINATED
已退出的线程处于此状态。
一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。
public class Test{
public static void main(String[] args){
Thread t = new Thread(()->{
for(int i=0;i<5;i++){
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
}
System.out.println("==============");
});
//观察状态
Thread.State s = t.getState();
System.out.println(s);
//启动后
t.start();
s = t.getState();
System.out.println(s);
while(s != Thread.State.TERMINATED){//
try{
Thread.sleep(100);
s = t.getState();
System.out.println(s);
}catch(Exception ex){
ex.printStackTrace();
}
}
//线程只能启动一次,关闭后不可再启动
//t.start();
}
}
stop
停止线程,通过线程对象调用。
public class Test{
public static void main(String[] args){
//主线程还要输出1~10
for(int i=1;i<=1000;i++){
if(i==35){
Thread.currentThread().stop();//方法已过期
}
System.out.println("main-----"+i);
}
}
}
这个方法已经过期了,不推荐使用,我们可以通过设置标志位来让线程正常停止
public class TestThread implements Runnable{
private boolean stop = false;
@Override
public void run(){
while(!stop){
System.out.println(Thread.currentThread().getName()+"-->"+(i++));
}
}
public void Stop(){
this.stop = true;
System.out.println(Thread.currentThread().getName()+"停止了");
}
public staic void main(String[] args){
TestThread tt = new TestThread();
Thread t = new Thread(tt,"子线程");
t.start();
//线程运行一段时间,这里可以写主线程的任务
for(int i = 0;i<=1000;i++){
if(i==500){
//修改标志位,停止线程
tt.Stop();
}
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
线程安全问题
在买票案例时,还是会出现多张10,出现-1,0等现象,出现这种现象是因为在线程进行“减减”操作前,其他线程强占了资源。
一个线程没执行完,其他线程就进行了抢占。
我们可以一再写一个例子银行取钱,同样也有这样的错误:
public class Test{
public static void main(String[] args){
Account a = new Account(100,"账户");
Draw d1 = new Draw(a,50,"我");
Draw d2 = new Draw(a,100,"朋友");
d1.start();
d2.start();
}
}
//账户
class Account {
public int money;
public String name;
public Account(int money, String name){
this.money = money;
this.name = name;
}
}
class Draw extends Thread{
private Account account;//账户
private int drawMoney;//取走的钱
public Draw(Account account,int drawMoney,String name){
super(name);//线程名
thia.account = account;
this.drawMoney = drawMoney;
}
@Override
public void run(){
if(account.money - drawMoney<0){
System.out.println(this.getName()+"钱不够");
}
//给予于时间暴露问题
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStrackTrace();
}
account.money = account.money - drawMoney;
System.out.println(this.getName()+"取走:"+drawMoney);
System.out.println("余额:"+account.money);
}
}
运行此程序,发现余额为-50。
解决:加入锁、同步代码块
锁的引入
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized。
当一个线程获得对象的排它锁(又称为写锁((eXclusive lock,简记为X锁)),若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。),独占资源,其他线程必须等待,使用后释放锁即可,存在下述问题:
-
一个线程持有锁会导致其他所有需要此锁的线程挂起;
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
-
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候需要线程同步,线程同步是一个等待机制,多个需要同时访问此对象的线程进入这个 对象的等待池 形成队列,等待前面线程使用完毕,下一个线程再使用。
同步代码块
加工买票类,将具有安全隐患的代码包裹起来。
public class BuyTicketThread implements Runnable{
public int ticketNum = 10;
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
synchronized(this){
if(ticketNum>0){
System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
}
}
以上代码修改的是实现Runable接口,this指代的是当前对象,由this来充当这把锁。
继承Thread类也可以做同样改动,同样也由this充当锁。
public class BuyTicketThread extends Thread{
private static int ticketNum = 10;
public TestThread(String name){
super(name);
}
public TestThread(){}
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
synchronized(this){
if(ticketNum>0){
System.out.println("从第"+super.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
}
}
运行main类
如果锁住的话,这个票应该是按顺序的,但结果并不是,所以this这把锁并没有锁住。
为什么哪?是因为this指代对象不同
- 实现Runable中,this指代同一对象。开启线程时我们调用的是同一Runable实现类对象。
- 继承Thread类,this指代不同对象。开启线程时我们调用的是不同的Thread子类对象。这就像上厕所,但每个人认为厕所有人的标准都不同,他认为红色是有人,他认为绿色是有人。
所以在继承Thread类中,锁设置一个常量"zzb"或者类的字节码信息"BuyTicketThread.class"即可。
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
synchronized(BuyTicketThread.class){
if(ticketNum>0){
System.out.println("从第"+super.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
}
注意
https://www.bilibili.com/video/BV1aw411o7rr?p=299&t=720.4
同步监视器总结:
synchronized(同步监视器){}
- 必须是引用数据类型,不能是基本数据类型
- 可以创建一个专门的同步监视器,没有任何业务意义
- 一般使用共享资源作为同步监视器
- 在同步代码块中不能改变同步监视器对象的引用
- 尽量不使用String和包装类作为同步监视器
- 建议使用final修饰同步监视器
同步代码块执行过程:
- 第一个线程来到同步代码块,发现同步监视器open状态,需要close关闭代码块,然后执行其中的代码
- 第一个线程执行过程中,发生了线程切换(阻塞就绪),第一个线程失去了cpu,但是没有开锁open
- 第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
- 第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
- 第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!!但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
多个代码块使用同一同步监视器(锁),锁住一个代码块的同时,也会锁住使用该同步监视器的其他代码块,其他线程无法访问其中的任何一个代码块
多个代码块使用同一同步监视器(锁),锁住一个代码块的同时,也会锁住使用该同步监视器的其他代码块,但是没有锁住使用其他同步监视器的代码块,这些代码块其他线程仍可访问
同步方法
加工买票类,将具有安全隐患的代码包裹起来
public class BuyTicketThread implements Runnable{
public int ticketNum = 10;
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
buy();
}
}
public synchronized void buy(){
if(ticketNum>0){
System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
继承Thread类也可以做同样改动,但是要将方法设为静态的,这样不同的对象就会调用同一方法了。
public class BuyTicketThread extends Thread{
private static int ticketNum = 10;
public TestThread(String name){
super(name);
}
public TestThread(){}
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
buy();
}
}
public static synchronized void buy(){//设为静态
if(ticketNum>0){
System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。锁一般都是引用数据类型的。
关于同步方法:
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this
静态同步方法的同步监视器是类名.class字节码信息对象 - 同步代码块的效率要高于同步方法了
原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部 - 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用真他监视器的代码块
Lock
Lock是一个接口
public class BuyTicketThread implements Runnable{
public int ticketNum = 10;
Lock lock = new ReenttrantLock();
@Override
public void run(){
//每个窗口都有100个人在买票
for(int i = 0;i<100;i++){
lock.lock();//打开锁
try{
buy();
}catch(Exception ex){
ex.printStackTrace();
}finally{
lock.unlock();//关闭锁
}
}
}
public void buy(){
if(ticketNum>0){
System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
}
}
}
Lock与synchronized的区别:
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
选取的优先顺序:
Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)
线程同步中的优缺点
对比:
线程安全,效率低
线程不安全,效率高I
可能造成死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
死锁问题解决减少同步资源的定义,避免嵌套同步
线程通信
阶段一
创建产品类,包括品牌名,与商品名
public class Product{
private String brand;
private String name;
public String getBrand(){
return this.brand;
}
public void setBrand(String brand){
this.brand = brand;
}
public String getName(){
return this.brand;
}
public void setName(String name){
this.name = name;
}
}
创建生产者进程,创建十个产品,在适当位置添加sleep时间,使问题在运行时暴露出来。
public class ProducerThread extends Thread{
private Product p;
public ProducerThread(Product p){
this.p = p;
}
@Override
public void run(){
for(int i=0;i<10;i++){
if(i%2 == 0){
p.setBrand("哈尔滨");
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
p.setName("啤酒");
}else{
p.setBrand("德芙");
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
p.setName("巧克力");
}
System.out.println("生产者生产了:"+p.getBrand()+"---"+p.getName());
}
}
}
创建消费者进程
public class ConsumerThread extends Thread{
private Product p;
public ProducerThread(Product p){
this.p = p;
}
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("消费这消费了:"+p.getBrand()+"---"+p.getName());
}
}
}
测试类
public class Test{
public static void main(String[] args){
Product p = new Product();
ProducerThread pt = new ProducerThread(p);
ConsumerThread ct = new ConsumerThread(p);
pt.start();
ct.start();
}
}
如此执行,产生了两个问题:
- 生产者与消费者没有交替执行
- 打印错误,哈尔滨—null,这是没有同步产生的问题,生产者生产到中途,被消费者抢占了。
阶段二
解决同步问题(问题2)。
同步代码块
修改生产者,修改时同步监视器的选取很重要,当生产者执行时,消费者不能进行执行,反之亦然。所以要同时锁住,this肯定不行,因为我们这里是继承了Thread类,线程开启是使用到了两个完全不同的对象。在同步监视器的选取中提到,可以选取共享资源,所以这里使用产品p作为同步监视器。
在被锁住线程运行时,若被锁住线程需要处CPU外其他资源进入阻塞状态,那么其他线程仍可以抢占(CPU),但是同锁线程不能运行。
public class ProducerThread extends Thread{
private Product p;
public ProducerThread(Product p){
this.p = p;
}
@Override
public void run(){
for(int i=0;i<10;i++){
synchronized(p){
if(i%2 == 0){
p.setBrand("哈尔滨");
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
p.setName("啤酒");
}else{
p.setBrand("德芙");
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
p.setName("巧克力");
}
System.out.println("生产者生产了:"+p.getBrand()+"---"+p.getName());
}
}
}
}
消费者
public class ConsumerThread extends Thread{
private Product p;
public ProducerThread(Product p){
this.p = p;
}
@Override
public void run(){
for(int i=0;i<10;i++){
synchronized(p){
System.out.println("消费这消费了:"+p.getBrand()+"---"+p.getName());
}
}
}
}
运行发现,打印错误问题得到解决,顺序仍有错误。
同步方法
也要重视锁的问题,若将方法提出来,改成同步方法,这样调用时生产者与消费者线程的同步监视器还是不同的,还是锁不住。所以还是要抓住共享的产品类,在产品类中添加同步方法。
public class Product{
private String brand;
private String name;
public String getBrand(){
return this.brand;
}
public void setBrand(String brand){
this.brand = brand;
}
public String getName(){
return this.brand;
}
public void setName(String name){
this.name = name;
}
//生产商品
public synchronized void SetProduct(String brand, String name){
this.setBrand(brand);
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了:"+this.getBrand()+"---"+this.getName());
}
//消费商品
public synchronized void GetProduct(){
System.out.println("消费这消费了:"+this.getBrand()+"---"+this.getName());
}
}
public class ProducerThread extends Thread{
private Product p;
public ProducerThread(Product p){
this.p = p;
}
@Override
public void run(){
for(int i=0;i<10;i++){
if(i%2 == 0){
p.SetProduct("哈尔滨","啤酒");
}else{
p.SetProduct("德芙","巧克力");
}
}
}
}
public class ConsumerThread extends Thread{
private Product p;
public ProducerThread(Product p){
this.p = p;
}
@Override
public void run(){
for(int i=0;i<10;i++){
p.GetProduct();
}
}
}
阶段三
解决交替问题,要生产之后在消费。
在同步方法上进行修改
public class Product{
private String brand;
private String name;
private boolean flag = false;
public String getBrand(){
return this.brand;
}
public void setBrand(String brand){
this.brand = brand;
}
public String getName(){
return this.brand;
}
public void setName(String name){
this.name = name;
}
//生产商品
public synchronized void SetProduct(String brand, String name){
if(flag){//有商品,等待消费
try{
wait();
}catch(Excption ex){
ex.printStackTrace();
}
}
this.setBrand(brand);
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了:"+this.getBrand()+"---"+this.getName());
//生产完
flag = true;//有商品
//通知消费者来消费
notify();
}
//消费商品
public synchronized void GetProduct(){
if(!flag){//有商品,等待生产
try{
wait();
}catch(Excption ex){
ex.printStackTrace();
}
}
System.out.println("消费这消费了:"+this.getBrand()+"---"+this.getName());
//消费完
flag = false;//没有商品
//通知生产者来生产
notify();
}
}
在Java对象中,有两种池
锁 池:synchronized
等待池:wait(),notify(),notifyAll()
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放),如果未来的某一时刻,另外一个线程调用了相同对象的notify方法或者notifyAll方法,那么该等待池中的线程就会被唤起,然后进入到对象的锁池里面去获得该对象的锁,如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait方法之后
等待池的方法wait,notify,notifyAll必须要在同步代码块或者同步方法中才可以,否则会报错。
sleep不释放锁,wait释放锁。notify唤醒一个,notifyAll唤醒全部。
Lock下的线程通信
以上情况只有一个生产者一个消费者,并且都在一个等待池中,那么唤醒的就不一定是谁了。所以可以是生产者与消费者在不同的等待池中。
Condition是在lava1.5中才出现的,它用来替代传统的Object的wait、notify实现线程间的协作,相比使用Object的wait、notify,使用Condition的await、signal这种方式实现线程间协作更加安全和高效。
它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock(同步器)拥有一个同步队列和多个等待队列。
Object中的wait,notify,notifyAll方法是和“同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与“互斥锁"/”共享锁“捆绑使用的。
调用Condition的await、signal、signalAll方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock()之间才可以使用
- Conditon中的await对应Object的wait;
- Condition中的signal对应Object的notify;
- Condition中的signalAll对应Object的notifyAll;
同一个锁lock,可以通过newCondition来创建等待池。
public class Product{
private String brand;
private String name;
private boolean flag = false;//false没有商品
//声明一个锁
private Lock lock = new ReentranLock();
Condition producerCondition = lock.newCondition();
Condition consumerCondition = lock.newCondition();
public String getBrand(){
return this.brand;
}
public void setBrand(String brand){
this.brand = brand;
}
public String getName(){
return this.brand;
}
public void setName(String name){
this.name = name;
}
//生产商品
public void SetProduct(String brand, String name){
lock.lock();
try{
if(flag){//有商品,等待消费
try{
producerCondition.await();//进入等待池,并释放锁
}catch(Excption ex){
ex.printStackTrace();
}
}
this.setBrand(brand);
try{
Thread.sleep(1000);
}catch(Exception ex){
ex.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了:"+this.getBrand()+"---"+this.getName());
//生产玩商品
flag = true;
//唤醒消费池中线程
consumerCondition.signal();
}catch(Exception ex){
ex.printStackTrace();
}finally{
lock.unlock();
}
}
//消费商品
public void GetProduct(){
lock.lock();
try{
if(!flag){//有商品,等待生产
try{
consumerCondition.await();
}catch(Excption ex){
ex.printStackTrace();
}
}
System.out.println("消费这消费了:"+this.getBrand()+"---"+this.getName());
//消费完
flag = false;//没有商品
//唤醒生产者来生产
producerCondition.signal();
}catch(Exception ex){
ex.printStackTrace();
}finally{
lock.unlock();
}
}
}