关于多线程的文章我已经写过一篇了,为什么还要再写一篇呢,因为上一篇《讲给女朋友听的java多线程》写的太生涩,也有一定小错误,女朋友并没有看懂。所以,博主又肝了一天,写了这篇通俗易懂,有可爱配图的多线程文章。
各位大佬,拿去给女朋友看吧。什么?没有女朋友?那就赶紧学java吧,new一个对象出来。
初入江湖的小李,在见识了“南慕容,北乔峰”的飒爽英姿之后,就励志成为一位武林高手。
小李开始了日复一日的修炼,然而在修炼了一年之后,小李的进境缺很慢,小李百思不得其解,正在恼怒之际,一位仙风道骨的老人传授他一门心法,名叫“多线程”,这门功法的强大之处就在于可以分心多用,同时修炼多种功法,这样一来,小李的进境就很快。
然而,这门心法缺有一个很大的缺陷,“线程同步问题”,就是在运行心法的时,如果多门功法需要同时使用丹田,这时,如果不小心就会走火入魔。
通过阅读本文,您好将掌握如下知识点:
本小节介绍什么是程序,什么是进程,什么又是线程,这些概念是本章的基础,刚开始接触,可能会觉得这些概念晦涩难懂,没关系,可以结合后面的实例来理解这些概念。
注意:线程的概念不必死记硬背,可以在完成本章的学习之后,再来回顾这些概念,你就回你有豁然开朗的感觉。
注意:
我们可以看到,小汽车是是静态的,即:程序时静态的。而运行中的发动机是动态的,即:进程是动态的。
由上面的三个图片,我们可以抽象出程序,进程,线程的概念:
注意:
现在,我们的电脑、手机都支持并发的执行程序,比如,我们在逛淘宝的时候,手机还放着音乐,我们在敲代码时,后台可能还运行着API文档,以备我们查看。
这些程序好像是在同时运行,然而并不是,对一个CPU而言,每个时刻,只能有一个程序在运行,但是CPU会在多个程序之间进行切换,而CPU的切换速度又很快,我们就感觉好像是这些程序在同时执行。
单线程的程序往往功能十分有限,例如:开发一个服务器程序,这个服务器程序需要向不同的客户端提供服务,不同的客户之间应该互不干扰,否则这个程序将不会被接收。
单线程程序只有一个顺序执行流,多线程则可以包括多个顺序执行流,多个线程之间互不干扰。
多线程程序的优点:
何时需要多线程:
在了解了线程的概念之后,我们在本节来创建线程。Java中使用Thread类表示线程,每一个线程都必须是Thread类的对象或其子类的对象。每个线程对象对应一定的任务,这些任务往往是需要被同时执行的。
Java中一共提供了四种创建线程的方式。分别是:继承Tread类,实现Runnable接口,实现Callable接口,使用线程池。其中,后两种是JDK5新增的创建线程的方式。
注意:使用者四种方式创建多线程时,自己多加思考,体会其中的异同。
多线程的创建:方法一:继承Thread类
步骤:
注意:
① 需要通过对象来启动线程。
② start方法会自动调用当前线程的run方法。不能直接调用run方法。
③ 不能让已经start的线程再去执行,会报异常。需要再去创建一个线程对象,通过这个对象再start。
下面这个例子创建两个线程:
① 一个线程用来输出偶数
创建一个继承自Thread的类,在类中重写run()方法,run()方法中写在线程被调度是执行的操作,比如:这里我们在run()方法内部输出100以内的偶数。
② 一个线程用来输出奇数
我们在main方法中使用匿名内部类的方式创建一个线程,用来输出100以内的奇数,和上面的基本一致,在类中重写run()方法,在run()方法内部写需要执行的操作。
程序清单如下
class Test extends Thread{
@Override
public void run()
{
for(int i=0;i<100;i++)
{
//判断是偶数
if(i%2==0)
{
System.out.println("偶数:"+i);
}
}
}
}
public class Demo1 {
public static void main(String[] args)
{
//创建一个输出偶数的线程
Test test = new Test();
//启动该线程
test.start();
Thread.currentThread().setName("主线程");//给main程序命名
//创建Thread类的匿名子类
new Thread()
{
@Override
public void run()
{
for(int i=0;i<100;i++)
{
//判断是奇数
if(i%2!=0)
{
System.out.println(Thread.currentThread().getName()+"匿名奇数:"+i);
}
}
}
}.start();
}
}
Thread类的有关方法:
① 启动线程,并执行对象的run()方法
void start();
② run()方法需要被重写,线程在被调度时执行的操作
void run();
③ 返回线程的名称
String getName();
④ 设置该线程名称
void setName(String name);
⑤ 返回当前线程。
static Thread currentThread();
⑥ 线程让步,释放CPU使用权,有可能在释放之后有被分配到使用权。
static void yield();
⑦ 线程阻塞。
join();
⑧ 让当前线程睡眠(指定时间:毫秒)
static void sleep(long millis);
⑨ 强制线程生命期结束,不推荐使用。(已经过时了)
stop();
⑩ 判断线程是否还活着,返回boolean
boolean isAlive();
注意:这些方法暂时不要全部掌握,通过后面的学习,在实践中慢慢使用,就会融会贯通。
多线程的创建:方法二:实现Runnable接口
步骤:
和方法一基本类似,创建一个类去实现Runable接口,在这个类中实现run()方法,把需要执行的操作写在run()方法体中。
程序清单如下
class Test3 implements Runnable{
@Override
public void run()
{
for(int i=0;i<100;i++)
{
if(i%2==0)
{
System.out.println("偶数:"+i);
}
}
}
}
public class Demo1 {
public static void main(String[] args)
{
Test3 test3 = new Test3();
Thread t = new Thread(test3);
t.start();
}
}
两个创建线程的比较:
实现Runnable的方式更好一点。因为实现它的类可能还有其他的直接父类,导致不能继承Thread。因为java是单继承的,但是可以同时有继承和实现。实现的方式会默认共享数据。
所以,在开发中,优先使用实现Runnable的方式。
相同点:都需要重写run方法。继承的方式在内部也实现的Runable接口。
多线程的创建:方法三:实现Callable接口
注意:此方法为JDK5.0新增的创建线程的方式。
步骤:
特点:
① 与使用Runnable相比, Callable功能更强大些
② 相比run()方法,可以有返回值
③ 方法可以抛出异常
④ 支持泛型的返回值
⑤ 需要借助FutureTask类,比如获取返回结果 。
⑥ get方法的返回值即为FutureTask构造器参数Callable实现类对象重写的call方法的返回值。
如何理解实现Callable接口创建多线程比实现Runnable接口创建多线程的方式更强大?
① call()方法可以有返回值。
② call()方法可以抛出异常,被外面的操作捕获,然后处理异常
③ call()方法支持泛型
在下面例子中,我们创建一个线程来获取100以内所有偶数的和。
首先,我们创建一个类实现Callable接口,并实现call()方法,同样,把该线程需要执行的操作放在call()方法内部。Call()方法会在线程启动时,被自动调用。Call()方法可以理解为一个增强版的run()方法。
程序清单如下
class NewThread implements Callable{
@Override
public Object call() throws Exception
{
int sum=0;
for(int i=1;i<=100;i++)
{
//判断是偶数
if(i%2==0)
{
sum=sum+i;
}
}
//自动装箱
return sum;
}
}
public class ThreadNew {
public static void main(String[] args)
{
NewThread newThread = new NewThread();
FutureTask futureTask = new FutureTask(newThread);
Thread t1 = new Thread(futureTask);
t1.start();
try
{
//get方法只是为了返回call方法的返回值。
//get方法自动调用newThread类对象的call()方法
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
}
背景:
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
比如,我们开发一个类似美团外卖的APP,用户在浏览页面时,需要在加载店家信息的同时加载美食的图片,这时就需要使用多线程,一个线程用来加载文字信息,一个线程用来加载图片。
这时,饥肠辘辘的用户就有可能浏览页面很快,如果使用以上三种方式来创建线程,就会发生,图片信息和文字信息加载不同步的问题,因为线程的创建也需要一定的开销。这会大大影响我们软件的体验感。
解决思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具,我们需要时就能直接使用,而不用等到它创建出来。
使用线程池的好处:
① 提高响应速度(减少了创建新线程的时间)
② 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③ 便于线程管理
corePoolSize
:核心池的大小
maximumPoolSize
:最大线程数
keepAliveTime
:线程没有任务时最多保持多长时间后会终止
步骤:
例:
在下面例子中,我们使用实现Runnable接口的方式创建两个线程,并把这两个线程加入到线程池中,一个线程用来输出偶数,一个线程用来输出奇数。
先创建一个容量为10线程池,把我们创建的两个线程加入到线程池中,加入到线程池中的线程会被自动调用,线程执行结束之后,该线程并不会死亡,而是将该线程返还给线程池以备下次使用。
程序清单如下
class NewThread5 implements Runnable{
@Override
public void run()
{
for(int i=0;i<100;i++)
{
if(i%2==0)
{
System.out.println(Thread.currentThread().getName()+"偶数:"+i);
}
}
}
}
class NewThread6 implements Runnable{
@Override
public void run()
{
for(int i=0;i<100;i++)
{
//判断是奇数
if(i%2!=0)
{
System.out.println(Thread.currentThread().getName()+"奇数:"+i);
}
}
}
}
public class NewThread4 {
public static void main(String[] args)
{
//service是线程池
ExecutorService service = Executors.newFixedThreadPool(10);
NewThread5 newThread5 = new NewThread5();
NewThread6 newThread6 = new NewThread6();
//输出偶数的线程
//适合于Runable
service.execute(newThread5);
//输出奇数的线程
service.execute(newThread6);
//适合Callab
// service.submit()
//关闭线程池
service.shutdown();
}
}
本节,我们将理解线程的调度问题。计算机通常只有一个cpu,而在任意时刻只能执行一条指令,每个线程只有获得cpu的使用权才能执行指令。而我们有需要使用多线程,那么计算机是如何实现的呢?
所谓多线程的并发运行,其实是从宏观上看。在计算机内部,各个线程轮流获取cpu的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待cpu,java虚拟机的一项任务就是负责线程的调度。
线程调度是指按照特定机制为多个线程分配CPU的使用。
调度策略:
① 时间片:同优先级线程组成先进先出队列(先到先服务),使用时间片策略
② 对高优先级,使用优先调度的抢占式策略
线程的优先级:
MAX_PRIORITY
:10
MIN _PRIORITY
:1
NORM_PRIORITY
:5
方法:
返回线程优先值
getPriority();
改变线程的优先级。
setPriority(int p);
例:设置为最大优先级
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
说明:
高优先级的程序要抢占低优先级CPU的执行权,但只是从概率上讲,高有优先级的程序高概率被执行。并不意味着只有当高优先级执行完之后才执行低优先级。
线程的分类
Java中的线程分为两类:
1. 一种是守护线程。
2. 一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
thread.setDaemon(true)
可以把一个用户线程变成一个守护线程。 Java垃圾回收就是一个典型的守护线程。人有生老病死,线程也和人一样,同样具有生命周期。线程的生命周期包含五种状态,分别是:新建,就绪,运行,阻塞,死亡。通过上面几节的学习,我们知道了如何创建线程,以及线程的调度问题。本节,我们将看到一个线程的生老病死。
|
|
|
线程的新建和就绪就类似我们人类的出生和大学毕业。
注意:
就绪状态是使用的start()方法,不是直接调用的run()方法,如果直接调用run()方法,那它仅仅是一个普通对象调用run()方法,并没有启动线程。
程序清单如下
class Test3 implements Runnable{
@Override
public void run()
{
for(int i=0;i<100;i++)
{
if(i%2==0)
{
System.out.println("偶数:"+i);
}
}
}
}
public class Test {
public static void main(String[] args)
{
Test3 test3 = new Test3();
Thread t = new Thread(test3);
//没有启动线程,系统只会把它当做一个普通对象。
t.run();
}
}
通过上面的实例,我们可以看到,启动一个线程的正确方法是调用线程对象的start()方法,该方法会自动调用run()方法。如果直接调用线程对象的run()方法,系统就只会吧它当做一个普通对象,这时,多线程就失去的它存在的意义,当前程序就会变成一个单线程程序。
线程的运行状态,就恰好对应我们大学毕业找到了一份好工作,在勤勤恳恳的努力着。
线程的阻塞状态,对应到我们身上就是失业的时候,这时候,就需要重新进入就绪状态,比如我们学习了新知识,然后再去找工作,线程就比较懒了,它只是在等CPU的重新调度或其他线程的帮助。
当发生如下情况时,线程会进入阻塞状态。
① 线程调用sleep()方法时,表示当前线程主动放弃CPU的执行权
② 当前的同步监视器被其他线程获得,该线程只能处于阻塞状态。
③ 当前线程在等待某个线程的notify().
对于以上情况,在阻塞结束时,该线程会重新进入就绪状态。
① sleep()指定时间已过
② 线程成功获得了同步监视器
③ 线程得到了其他线程的通知。
线程在以下情况下会死亡:
① run()方法或call()方法执行完毕
② 线程执行过程中调用的stop()方法,强制让该线程死亡,但该方法容易发生死锁。
③ 线程捕获了一个异常。
使用isAlive()方法检测一个线程是否死亡,当线程处于新建和死亡两种状态时,该方法返回false,当线程处于就绪、运行、阻塞状态时,该方法返回true。
注意:不要对已经死亡的线程调用start()方法使其复活,这是不可能的。
在单线程中,每次只完成一个任务,当然没有什么安全问题。就像一个人专心致志做一件事情时,就很少发生做错或忘了某步骤。而在一心多用时,就有很大的几率发生做错或做的很不完美的的事情。同样,在多线程中,也存在这样类似的问题 — 数据共享。
我们通过一个例子来引如线程同步的问题,现在,我们设计一个程序,模拟车站卖票,为了贴近真实生活,我们创建三个窗口同时卖票,然而又恰逢春运期间,只有一百张票。
我们使用继承Thread类和实现Runnable接口的两种方式来创建窗口线程。
一、使用继承Thread类创建线程
程序清单如下
class Window extends Thread{
//票数 只有100张
private int ticket=100;
@Override
public void run()
{
while (true)
{
//当前票数大于0,就卖票
if(ticket>0)
{
System.out.println(Thread.currentThread().getName()+":"+"卖票,票号为:"+ticket);
ticket--;
}
else {
break;
}
}
}
}
public class Test {
public static void main(String[] args)
{
//new了三个Windowd对象,表示三个窗口
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
//设置三个窗口线程的名字
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
//启动三个窗口线程,开始卖票
w1.start();
w2.start();
w3.start();
}
}
运行结果如下
这是程序运行结果的部分截图,我们可以看到,会出现重票的情况,这就情况肯定是不允许发生的。
为什么会出现这种情况,读者可能会发现,我们上面new了三个Window线程对象,是不是每个线程对象都持有100张票呢?我们继续看下面这个例子,它只new出一个Window对象。
二、实现Runnable接口创建多线程
程序清单如下
class Window2 implements Runnable{
private int ticket=100;
@Override
public void run(){
while (true)
{
//票数大于0,则卖票
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":"+"卖票,票号为:"+ticket);
ticket--;
}
else {
break;
}
}
}
}
public class Test {
public static void main(String[] args)
{
//因为只new了一个Window对象,所有共用一个ticket
Window2 w2 = new Window2();
//创建三个线程
Thread t1 = new Thread(w2);
Thread t2 = new Thread(w2);
Thread t3 = new Thread(w2);
//设置线程名
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//启动线程,开始卖票
t1.start();
t2.start();
t3.start();
}
}
运行结果如下
从这个运行结果看,好像大部分的重票问题解决了,但是还是会有三张100的票,这显然也是不行的。
至此,我们可以得出,在使用多线程处理问题时,特别是有共享数据时。会发生线程安全问题。
我们分析出现线程安全的原因:
当某个线程操作车票的过程中,尚未完成操作,其线程也参与进来操作车票,这就导致了重票问题的发生
解决思路:
当一个线程A在操作车票时,其他线程不能参与进来,当A操作完成后,其他线程再操作。即使线程A出现阻塞,也不能改变。
Java中通过线程同步来解决线程安全问题。
线程同步有三种方法,分别是:
① 同步代码块
② 同步方法
③ Lock锁
在上面模拟窗口卖票的程序中,由于run()中的卖票行为可以同时被多个线程执行,导致了线程安全问题。现在,我们限制对卖票行为的访问,同一时间只允许一个线程对其进行操作。我们可以使用同步代码块的方法,实现这个限制。
格式:
synchronized(obj){
//需要被同步的代码,即:操作共享数据的代码
}
这段代码的含义是,在执行对共享数据的操作时,要首先获得同步监视器,简称:锁。
而且同一时间只能有一个线程获得同步监视器,其他没有获得同步监视器的线程则处于阻塞状态,直到该线程释放同步监视器,其他线程才可以获得同步监视器,进而对共享数据进行操作。
理解两个概念:共享数据和同步监视器
① 共享数据:多个线程共同操作的变量,例如:上面程序中的车票ticket。
② 同步监视器:俗称“锁”。一个监视器就相当于一扇门,里面锁着的是共享的资源,每次只能有一个人能进入,并且只能容纳一个人,也就是说只有一个线程能获得这个锁。
同步监视器的要求:
① 任何一个类的对象都可以充当一个锁。
② 多个线程共用同一个锁。
③ 推荐使用共享数据充当锁。
注意:
① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
② 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),this的使用需谨慎,确保是同一个对象才可以。
③ 在实现Runnable接口创建多线程的方式中,我们可以使用this充当锁来代替手动new一个对象,因为后面我们只创建了一个线程的对象。
④ 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。
同步的优缺点:
同步的范围:
现在,我们使用两种方式来解决上面卖票程序中出现的线程安全问题。
方法一:实现Runnable接口创建多线程解决线程安全问题:
程序清单如下
class Window2 implements Runnable{
private int ticket=100;
@Override
public void run()
{
while (true)
{
//同步代码块,因为我们创建了三个线程,所以不能使用this作同步监视器
synchronized(Window2.class)
{
//票数大于0,则卖票
if (ticket > 0)
{
try
{
//是当前线程sleep100毫秒,体现线程的等待
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket);
ticket--;
} else
{
break;
}
}
}
}
}
public class Test {
public static void main(String[] args)
{
//创建一个窗口对象
Window2 w1 = new Window2();
//创建三个线程
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
//设置线程名字
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
运行结果如下
我们看到,现在,已经没有重票的问题出现了。
方法二:继承Thread类创建多线程使用同步代码块解决问题的代码:
程序清单如下
class Window extends Thread{
//使用static修饰保证,保证上线程共用ticket
private static int ticket=100;
@Override
public void run()
{
while (true)
{
synchronized (Window.class)
{
//票数大于0,则卖票
if (ticket > 0)
{
try
{
sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket);
ticket--;
} else
{
break;
}
}
}
}
}
public class Test {
public static void main(String[] args)
{
//创建三个线程
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
//设置线程名字
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
//启动线程
w1.start();
w2.start();
w3.start();
}
}
运行结果如下
我们看到。线程安全问题同样被解决了。
与同步代码块相对应的,是同步方法。使用synchronized关键字修饰操作共享数据的的方法。该方法就被称为同步方法。即:如果操作共享数据的代码完整的声明在一个方法中,我们可以将这个方法声明为同步的。
同步方法仍然有同步监视器,只是不需要我们显示的声明。
① 非静态同步方法,同步监视器:this
② 静态同步方法,同步监视器:当前类本身
我们使用同步方法来解决懒汉式创建单例对象的线程安全问题。
程序清单如下
class Bank{
//无参构造器
private Bank()
{
}
//缓存单例类的对象
private static Bank instance=null;
//同步方法
public static synchronized Bank getInstance()
{
//表示还没有创建单例对象
if(instance==null)
{
instance = new Bank();
}
return instance;
}
}
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当,每次只能有一个线程对Lock对象加锁,线程开始操作共享数据之前,要先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
使用ReentrantLock对象来充当锁进行同步时,我们建议把释放锁的操作放在finally语句块中,确保一定会释放锁,防止死锁的出现。
Lock锁也遵循“加锁–修改–释放锁”的逻辑。
我们同样以卖车票的例子来说明。
程序清单如下
class Window5 implements Runnable{
private int ticket = 100;
//1. 实例化一个ReentrantLock对象
//默认参数是false,写为true之后,表示是公平的锁
private ReentrantLock lock = new ReentrantLock();
@Override
public void run()
{
while (true)
{
//2. 调用锁定方法。lock方法。获得锁
lock.lock();
try
{
//票数大于0 则卖票
if(ticket>0)
{
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket);
ticket--;
}
else
{
break;
}
}finally
{
//3. 解锁
lock.unlock();
}
}
}
}
public class Test {
public static void main(String[] args)
{
//创建窗口对象
Window5 w5 = new Window5();
//创建三个线程
Thread t1 = new Thread(w5);
Thread t2 = new Thread(w5);
Thread t3 = new Thread(w5);
//设置线程名字
t1.setName("窗口一:");
t2.setName("窗口二:");
t3.setName("窗口三:");
//启动三个线程
t1.start();
t2.start();
t3.start();
}
}
synchronized 与 Lock 的对比:
优先使用顺序:
Lock --> 同步代码块(已经进入了方法体,分配了相应资源)–> 同步方法(在方法体之外)
在同步代码块和同步方法中,何时会释放对同步监视器的锁定
以下情况不会释放同步监视器
总结:
解决线程安全问题,有几种方式?
① synchronized
同步代码块
同步方法
② Lock
我们来看一个典型例题,来加深对线程同步的理解
例题:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题分析:
① 是不是多线程问题?
肯定是,两个线程,分别是两个储户
② 是否有共享数据?
有,账户
③ 所以,存在线程安全问题。
使用同步机制解决
程序清单如下
class Account{
//余额
private double balance;
//使用继承的方式创建多线程,需要加static,保证是一把锁
private static ReentrantLock lock = new ReentrantLock();
public Account(double balance)
{
this.balance=balance;
}
//方法一:使用synchronized的同步方法
// public synchronized void deposit(double amt)
//方法二:使用lock
//存款
public void deposit(double amt)
{
//加锁
lock.lock();
try
{
//存款金额大于0,则存款
if(amt>0)
{
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
balance=balance+amt;
System.out.println(Thread.currentThread().getName()+":"+"存钱成功,余额为:"+balance);
}
}
finally
{
//释放锁
lock.unlock();
}
}
}
/**
* 为了演示这个程序可以使用this作为同步监视器,使用继承的方式创建多线程。
*/
class Customer extends Thread{
//账户
private Account account;
public Customer(Account account)
{
this.account=account;
}
@Override
public void run()
{
for(int i=0;i<3;i++)
{
//存款1000
account.deposit(1000);
}
}
}
public class Test {
public static void main(String[] args)
{
//创建一个账户
Account account = new Account(0);
Customer customer1 = new Customer(account);
Customer customer2 = new Customer(account);
customer1.setName("甲");
customer2.setName("乙");
customer1.start();
customer2.start();
}
}
运行结果如下
注意:
推荐使用实现的方式创建多线程。本程序为了演示可以使用this作为同步监视器,使用继承的方式创建多线程。
采用继承的方式创建多线程,使用同步方法时,慎用this,这个题可以使用this,是因为this是唯一的,是同一个Account。
什么是线程死锁,举个很形象的例子。
你和你的好朋友去吃饭,餐桌上有一盘十分美味的佳肴,你和你的朋友都想吃,但是只有一双筷子,你和你的朋友一人抢到一只筷子,你们都不愿意放弃这顿美味佳肴,都在等对方放弃筷子,这时,你们便一直僵持着。这就是死锁。
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续类似与死循环。
下面的程序就演示了线程死锁。
程序清单如下
public class Lock {
public static void main(String[] args)
{
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
//继承方式创建匿名多线程
new Thread()
{
@Override
public void run()
{
synchronized (s1)
{
s1.append("A");
s2.append("1");
try
{
//加大死锁概率
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
synchronized (s2)
{
s1.append("B");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
//实现方式创建匿名多线程
new Thread(new Runnable() {
@Override
public void run()
{
synchronized (s2)
{
s1.append("C");
s2.append("3");
synchronized (s1)
{
s1.append("D");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
这个程序运行时,不会出错,也不会报异常,就是一直僵持着,除非我们手动停掉程序的运行。
我们的程序中不应该出现线程死锁的问题,我们可以通过下面几种常见的方法来避免线程死锁。
当执行多线程程序时,CPU会在多个线程之间进行调度,而调度具有一定的随机性,我们无法在程序中准确控制线程之间的轮换执行。
比如,我们开发时,需要多个模块之间的协调,A模块需要B模块给出一个参数,这时,在执行A模块时,就必须转去执行B模块,这就是线程之间的通信。
本小节通过例题来说明线程之间的通信。
线程通信中的三个常用方法:wait(); 、 notify();、notifyAll();
wait();
是调用其的线程进入阻塞状态。并释放锁。notify();
唤醒被阻塞的线程。如果有多个线程,就唤醒优先级高的那个。notifyAll();
唤醒所有被阻塞的线程。注意:
以下程序会出现非法监视器的错误
程序清单如下
class Number implements Runnable{
private int number=1;
private ReentrantLock lock = new ReentrantLock();
//创建一个对象充当同步监视器
Object object = new Object();
@Override
public void run()
{
while (true)
{
synchronized (object)
{
//唤醒一个线程,默认是this调用。
notify();
if(number<101)
{
System.out.println(Thread.currentThread().getName()+"打印:"+number);
number++;
try
{
//使得调用如下wait方法的线程进入阻塞状态,wait会释放锁
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
else
{
break;
}
}
}
}
}
在这个程序中,我们new了一个Object类的对象充当同步监视器,在同步代码块中调用notifyAll()非法,默认通过this调用,而这里的同步监视器是Object对象,出现了不一致,所以会报非法同步监视器异常。
sleep()方法和wait()方法的异同:
我们通过一个典型的例题来理解线程的通信
例:
生产者和消费者的问题:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
问题分析:
① 是否是多线程?
是,生产者线程,消费者线
② 共享数据是什么?
店员(产品)
③ 如何解决线程安全问题?
同步机制,三种方法
线程的通信
程序清单如下
//店员
class Clerk{
private int amount=0;
//生产产品
public synchronized void producePorduct()//同步方法
{
//产品小于20时,生产者开始生产产品
if(amount<20){
amount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+amount+"个产品");
notify();
}
//产品大于20时,生产者停止生产,等待消费者消费
else{
//等待
try
{
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct()
{
//当产品数大于0时,即:当前有产品时,消费者开始消费产品
if(amount>0)
{
System.out.println(Thread.currentThread().getName()+":开始消费第"+amount+"个产品");
amount--;
notify();
}
//当前已经没有产品时,消费者等待生产者生产
else
{
//等待
try
{
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
//生产者线程
class Producer implements Runnable{
//店员对象
private Clerk clerk;
public Producer(Clerk clerk)
{
this.clerk=clerk;
}
@Override
public void run()
{
System.out.println(Thread.currentThread().getName()+"开始生产……");
while (true)
{
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
clerk.producePorduct();//生产者生产产品
}
}
}
//消费者线程
class Consumer implements Runnable{
private Clerk clerk;
public Consumer(Clerk clerk)
{
this.clerk=clerk;
}
@Override
public void run()
{
System.out.println(Thread.currentThread().getName()+"开始消费……");
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
//消费者消费产品
clerk.consumeProduct();
}
}
}
public class Test {
public static void main(String[] args)
{
//创建一个店员对象
Clerk clerk = new Clerk();
//创建生产者对象
Producer p1 = new Producer(clerk);
//创建消费者对象
Consumer c1 = new Consumer(clerk);
//创建生产者线程
Thread t1 = new Thread(p1);
t1.setName("生产者");
//创建消费者线程
Thread t2 = new Thread(c1);
t2.setName("消费者");
//启动线程
t1.start();
t2.start();
}
}
运行结果如下