初识多线程
进程和线程:
(1)什么是进程
1)例如记事本酷狗音乐等软件,程序启动运行后,操作系统会给其分配系统资源和内存空间;进程是系统层面的资源调度和分配单位;
2)进程就是在内存中运行的应用程序,进程都有自己独立的内存空间,该进程在执行过程中可以独享该块内存空间,所以一个进程出问题,不会影响其他进程;
3)大部分简单的应用程序只会对应一个进程;某些复杂的如杀毒软件、数据库软件等是由多个进程组成的;
4)早期的操作系统是单任务的,同时只能运行一个程序;现在的操作系统大多支持多程序同时执行,这就是进程在起作用。
(2)什么是线程
1) 线程是进程的一个执行单元,其负责当前进程中某部分程序的执行。一个进程中至少有一个线程,一个进程也可以运行多个线程,多个线程间可共享数据;
2.1)多个线程是共享某进程的内存等资源的,一个线程没有独立的运行空间,一个线程出问题,会影响其他线程的正常运行;所以,多进程的程序比多线程的程序更健壮;但是,因为多进程切换时,资源损耗较大,所以多进程比多线程效率低。
2.2)与进程不同的是同类的多个线程共享进程的 堆 和 方法区 资源,但每个线程有自己的 程序计数器 、 虚拟机栈 和本地方法栈 ,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
(3)进程和线程的区别
(摘自:https://blog.csdn.net/ThinkWon/article/details/102021274)
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别 :进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配 :同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系 :一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
(4)线程提升效率的原因
【多线程能提高运行效率的前提】是线程本身不会一直占用CPU且不占CPU的空闲时间大于线程切换时间,否则在线程时间运行效率变低,在线程需100%占用CPU时,CPU利用率也不会得到提升。即线程是一种压榨CPU的一种方式,使CPU尽量一刻不得闲,从而提升效率。如果,CPU已经达到了极限,再使用线程是起不到作用的。
Thread类创建线程:
(1)Thread类的构造方法:
●Thread(Runnable target),正是通过Runnable接口创建线程时需要用到的构造方法;
(2)Thread类常用方法
●不同功能的线程的区别就在于run()方法中的代码不同;
(3)Thread类也是实现了Runnable接口的
(4)Thread类创建线程示例程序
Java规定一个类必须继承Thread类(或实现Runnable接口)才能成为一个线程类,Java就是这样设计和规定的。
简单示例:其中通过 MyThread myThread = new MyThread(); 实例化线程类得到一个线程,主方法又是一个线程;
(1)创建一个类继承Thread类,得到线程类;(2)线程类添加run()方法,在run方法中添加线程相关代码;(3)实例化线程类对象,调用start()方法启动线程;
// 一个类需要继承Thread类,才能成为一个线程类
class MyThread extends Thread{
// 所有和线程相关的代码都写在run()方法内
public void run(){
System.out.println("线程正在执行");
}
}
public class ThreadTest {
public static void main(String[] aegs){
MyThread myThread = new MyThread();
// 一个线程不能多次调用start()方法,即不能多次启动。其不会编译报错,但会产生运行时错误
myThread.start();
System.out.println("主方法的线程");
}
}
……………………………………………………
简单示例:同上;
(1)创建一个类继承Thread类,得到线程类;
(2)线程类添加run()方法;
(3)线程类添加有参构造方法,通过调用Thread类的有参构造,得线程类的有参构造,该有参构造方法可以指定线程的name;
(4)调用线程类的有参构造,MyThread mythread1 = new MyThread(“线程1”);创建线程;线程类每实例化一次就得到一个实际的线程;
(5)在线程类中可以直接调用getName()方法得到当前线程的name;
class MyThread extends Thread{
public MyThread(){}
// 定义一个构造方法,通过调用父类Thread类的构造方法,给线程指定一个name
public MyThread(String name){
super(name);
}
// 所有和线程相关的代码都写在run()方法内
public void run(){
for(int i = 1;i<=10;i++){
// 因为当前类是直接继承Thread类的,所以可以直接使用getName()方法,而无需通过实例化对象等方式调用
System.out.println(getName()+"执行第"+i+"次;");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
// 一个类继承了Thread类,并实现run方法后,该类便成了一个线程类,该类的每实例化一个对象,就是新创建了一个实际的线程。
MyThread mythread1 = new MyThread("线程1");
MyThread mythread2 = new MyThread("线程2");
mythread1.start();
mythread2.start();
}
}
Runnable接口创建线程
这种方式更常用,因为Java不支持多继承。
(1)Runnable接口说明
(2)示例程序
本例是最容易理解的形式:
可以发现(
1)Printrunnable类实现Runnable接口,并实现run()方法,成为线程类;
(2)通过Thread.currentThread().getName()获取线程name,因为Runnable接口中没有getName()方法,getName()方法实在Thread类的,所以通过Thread类去获取线程名;
(3)实例化线程类对象,通过Thread的有参构造:Thread(Runnable target),得到最终的线程对象;
注:通过(3)可以发现,只有Thread类的对象,或者Thread子类的实例对象,才是最终的可以启动的进程对象;一个实现了Runnable接口的类的实例对象,可以理解为是具备了线程潜质的对象,但其并不是一个可以启动的线程对象,其还是要通过Thread thread = new Thread(Runnable target)来得到最终的线程对象。
// PrintRunnable类实现Runnable接口
class PrintRunnable implements Runnable{
//int i = 1; // 多个线程处理同一个资源的情况
@Override
public void run() {
int i = 1;
while(i <= 10){
// 此时,不能直接使用getName()获取线程name了,因为getName()方法是Thread类里面的;
System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
}
}
}
public class Test {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
// Thread类的构造方法中有这种形式:Thread(Runnable target)
Thread t1 = new Thread(pr1);
t1.start();
// PrintRunnable pr2 = new PrintRunnable();
PrintRunnable pr2 = new PrintRunnable();
// Thread类的构造方法中有这种形式:Thread(Runnable target)
Thread t2 = new Thread(pr2);
t2.start();
}
}
输出结果:
Thread-1正在运行1
Thread-0正在运行1
Thread-0正在运行2
Thread-1正在运行2
Thread-0正在运行3
Thread-1正在运行3
Thread-0正在运行4
Thread-1正在运行4
Thread-1正在运行5
Thread-1正在运行6
Thread-1正在运行7
Thread-1正在运行8
Thread-1正在运行9
Thread-1正在运行10
Thread-0正在运行5
Thread-0正在运行6
Thread-0正在运行7
Thread-0正在运行8
Thread-0正在运行9
Thread-0正在运行10
……………………………………………………
对上示例改造如下:线程类只实例化一次,通过多次调用Thread thread = new Thread(Runnable target)得到多个线程;
// PrintRunnable类实现Runnable接口
class PrintRunnable implements Runnable{
//int i = 1; // 多个线程处理同一个资源的情况
@Override
public void run() {
int i = 1;
while(i <= 10){
// 此时,不能直接使用getName()获取线程name了,因为getName()方法是Thread类里面的;
System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
}
}
}
public class Test {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
// Thread类的构造方法中有这种形式:Thread(Runnable target)
Thread t1 = new Thread(pr1);
t1.start();
// PrintRunnable pr2 = new PrintRunnable();
// Thread类的构造方法中有这种形式:Thread(Runnable target)
Thread t2 = new Thread(pr1);
t2.start();
}
}
输出结果:由结果也可发现,同一个线程类的实例化对象,如果多次调用Thread thread = new Thread(Runnable target),也会得到不同的线程
Thread-1正在运行1
Thread-0正在运行1
Thread-0正在运行2
Thread-0正在运行3
Thread-0正在运行4
Thread-0正在运行5
Thread-1正在运行2
Thread-1正在运行3
Thread-1正在运行4
Thread-1正在运行5
Thread-1正在运行6
Thread-1正在运行7
Thread-1正在运行8
Thread-1正在运行9
Thread-1正在运行10
Thread-0正在运行6
Thread-0正在运行7
Thread-0正在运行8
Thread-0正在运行9
Thread-0正在运行10
……………………………………………………
对上示例改造如下:把循环变量i转移到循环外
// PrintRunnable类实现Runnable接口
class PrintRunnable implements Runnable{
int i = 1; // 多个线程处理同一个资源的情况
@Override
public void run() {
//int i = 1;
while(i <= 10){
// 此时,不能直接使用getName()获取线程name了,因为getName()方法是Thread类里面的;
System.out.println(Thread.currentThread().getName()+"正在运行"+(i++));
}
}
}
public class Test {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
// Thread类的构造方法中有这种形式:Thread(Runnable target)
Thread t1 = new Thread(pr1);
t1.start();
// PrintRunnable pr2 = new PrintRunnable();
// Thread类的构造方法中有这种形式:Thread(Runnable target)
Thread t2 = new Thread(pr1);
t2.start();
}
}
输出结果:多个线程处理同一个资源的情况,这是多个线程处理同一个资源的情况。之所以是多个线程处理同一个资源,是因为上面程序中,两个Thread t1 =new Thread(pr1)和Thread t2 = new Thread(pr1)使用了同一个Runnable类型的对象。
Thread-0正在运行1
Thread-1正在运行1
Thread-0正在运行2
Thread-1正在运行3
Thread-0正在运行4
Thread-0正在运行6
Thread-1正在运行5
Thread-0正在运行7
Thread-0正在运行9
Thread-0正在运行10
Thread-1正在运行8
1.线程的状态
●新建(New):当创建一个Thread类或Thread子类对象时,线程就进入了新建状态;
●可运行(Runnable):当创建好的线程对象去调用start()方法后,线程就进入了可运行状态;具体线程运行与否要看CPU有没有空,即这个线程获取了CPU的使用权,这个处于可运行状态的线程才会实际执行;有时,也称可运行状态为“就绪状态”。
●正在运行(Running):一个处于可运行状态的线程,一旦获取了CPU的使用权,就可进入正在运行状态;
●阻塞(Blocked):当线程遇到干扰时,会进入阻塞状态,进程处于暂停状态;
●终止(Dead)
2.线程的生命周期
线程的生命周期是线程从创建到运行一直到运行结束的过程;也是线程的五个状态的转换过程;可以通过调用Thread类的一些方法,来影响线程的状态。
●I/O请求需要耗费一定的时间,此时就可以让线程进入阻塞状态(暂时暂停),等待I/O请求完成,再继续进行执行;
●stop()方法现在不推荐用了,JDK中已标注为版本过期;
1.sleep()方法
●sleep()方法使线程由正在运行状态转换到阻塞状态;线程暂时放弃CPU使用权;
●sleep()方法是Thread类中的方法;
●public static void sleep(long millis):静态方法,可直接用Thread类名字去调用;millis:毫秒,1000ms == 1s;
●sleep()方法:应用场景可能有(1)需要计时的情况,每隔一定时间显示一下时间等;(2)对于一些需要定期刷新数据而不是一直刷新数据的情况;
●需要注意一个线程到底能不能执行,或者说一个线程到底能不能“暂停”,不仅和sleep()方法有关,还与CPU是否可用有关;当一个线程sleep()方法执行完毕后,线程只是进入了可运行状态,该线程必须获得CPU的使用权才能真正开始执行。(所以,sleep()方法应用在严格的时钟计算时,可能会存在误差);
……………………………………………………
示例程序:
注:sleep()方法需要捕获异常;
sleep()方法的具体应用场景和应用技巧需要在实际开发中慢慢积累 ;
class MyThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 1;i<=15;i++){
System.out.println(Thread.currentThread().getName()+"执行第"+i+"次。");
try {
// 为什么要捕获异常:在线程执行sleep()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class SleepDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread mt = new MyThread();
Thread thread = new Thread(mt);
thread.start();
//Thread thread1 = new Thread(mt);
//thread1.start();
}
}
2.join方法
●join()方法使线程由正在运行状态转换到阻塞状态;线程暂时放弃CPU使用权;
●join()方法也是Thread类的方法;
●public final void join():是个final方法,字面理解join是加入、抢先进入的意思;
●一个线程调用了join()方法,那么这个线程获得了优先执行权,其他线程需要等待该线程执行完毕后才能执行;join()方法是一种抢占资源的方式;
……………………………………………………
示例程序:join(),无参形式,调用join()方法线程执行结束后,其他线程才会执行。
注:join()方法也需要捕获异常;
join()方法的具体应用场景和应用技巧需要在实际开发中慢慢积累;
class MyThread1 extends Thread{
public void run(){
for(int i = 1;i<=10;i++){
System.out.println(getName()+"正在执行"+i+"次。");
}
}
}
public class JoinDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread1 mt = new MyThread1();
mt.start();
try {
// 为什么要捕获异常:在线程执行join()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
mt.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i = 1;i<=10;i++){
System.out.println("主线程运行第"+i+"次。");
}
System.out.println("主线程运行结束。");
}
}
输出结果:可以发现,因为mt线程调用了join()方法,所以mt线程获得了优先执行权;
Thread-0正在执行1次。
Thread-0正在执行2次。
Thread-0正在执行3次。
Thread-0正在执行4次。
Thread-0正在执行5次。
Thread-0正在执行6次。
Thread-0正在执行7次。
Thread-0正在执行8次。
Thread-0正在执行9次。
Thread-0正在执行10次。
主线程运行第1次。
主线程运行第2次。
主线程运行第3次。
主线程运行第4次。
主线程运行第5次。
主线程运行第6次。
主线程运行第7次。
主线程运行第8次。
主线程运行第9次。
主线程运行第10次。
主线程运行结束。
若是,mt线程没有调用join方法:
class MyThread1 extends Thread{
public void run(){
for(int i = 1;i<=10;i++){
System.out.println(getName()+"正在执行"+i+"次。");
}
}
}
public class JoinDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread1 mt = new MyThread1();
mt.start();
// try {
// // 为什么要捕获异常:在线程执行join()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
// mt.join();
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
for(int i = 1;i<=10;i++){
System.out.println("主线程运行第"+i+"次。");
}
System.out.println("主线程运行结束。");
}
}
输出结果:mt线程没有调用join()方法时,mt线程和主线程随机获取CPU使用权。
主线程运行第1次。
Thread-0正在执行1次。
主线程运行第2次。
Thread-0正在执行2次。
主线程运行第3次。
Thread-0正在执行3次。
Thread-0正在执行4次。
主线程运行第4次。
主线程运行第5次。
主线程运行第6次。
主线程运行第7次。
主线程运行第8次。
主线程运行第9次。
主线程运行第10次。
主线程运行结束。
Thread-0正在执行5次。
Thread-0正在执行6次。
Thread-0正在执行7次。
Thread-0正在执行8次。
Thread-0正在执行9次。
Thread-0正在执行10次。
……………………………………………………
示例程序:join(long millis):调用join(long millis)方法的线程,其优先执行权只能维持一定时间,一定时间过后丧失优先权,和其他线程一样随机获取执行权
class MyThread1 extends Thread{
public void run(){
for(int i = 1;i<=300;i++){
System.out.println(getName()+"正在执行"+i+"次。");
}
}
}
public class JoinDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread1 mt = new MyThread1();
mt.start();
try {
// 为什么要捕获异常:在线程执行join()方法时候,可能会因为某些原因线程突然被打断,要处理一下这种异常
mt.join(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i = 1;i<=10;i++){
System.out.println("主线程运行第"+i+"次。");
}
System.out.println("主线程运行结束。");
}
}
输出结果:输出结果截取部分,由执行结果可以发现join(long millis)方法作用。
Thread-0正在执行66次。
Thread-0正在执行67次。
Thread-0正在执行68次。
Thread-0正在执行69次。
主线程运行第1次。
Thread-0正在执行70次。
Thread-0正在执行71次。
主线程运行第2次。
Thread-0正在执行72次。
主线程运行第3次。
Thread-0正在执行73次。
主线程运行第4次。
Thread-0正在执行74次。
主线程运行第5次。
Thread-0正在执行75次。
主线程运行第6次。
Thread-0正在执行76次。
主线程运行第7次。
Thread-0正在执行77次。
主线程运行第8次。
Thread-0正在执行78次。
Thread-0正在执行79次。
Thread-0正在执行80次。
Thread-0正在执行81次。
Thread-0正在执行82次。
Thread-0正在执行83次。
Thread-0正在执行84次。
Thread-0正在执行85次。
Thread-0正在执行86次。
3.线程的优先级
●Java为线程提供了10个优先级,优先级可以使用整数1-10表示,超过这个范围会抛出异常;数字越大,表示优先级越高;
●main()方法的线程(主线程)默认优先级是5;
●除了用数字表示优先级,还可以用优先级常量来表示优先级:
MAX_PRIORITY:线程的最高优先级10;
MIN_PRIORITY:线程的最低优先级1;
NORM_PRIORITY:线程的默认优先级5;
●Thread类提供了获取线程优先级的方法、和设置线程优先级的方法:
●线程的优先级只是提供了一个优先级别,优先级高的线程不一定非得先执行,是否先执行与CPU状态,系统状态等很多因素有关;即其执行还是有一定的随机性的;
……………………………………………………
示例程序:主要就是setPriority()方法、和getPriority()方法;
class MyThread2 extends Thread{
private String name;
public MyThread2(){}
public MyThread2(String name){
// 注:给线程自定义名字,需要调用Thread的有参构造才行,即需要super(name)才行;
// 这儿的name只是一个方便观察的变量,其实并不是线程的name.
this.name = name;
}
public void run(){
for(int i = 1;i<=10;i++){
System.out.println("线程"+name+"正在执行"+i);
}
}
}
public class PriorityDemo {
public static void main(String[] args) {
// 获取主线程的优先级
int mainPriority = Thread.currentThread().getPriority();
//System.out.println("主线程的优先级为:"+mainPriority);
MyThread2 mt1 = new MyThread2("线程1");
MyThread2 mt2 = new MyThread2("线程2");
int mt1Priority = mt1.getPriority();
//System.out.println("mt1线程的优先级为:"+mt1Priority);
// 设置mt1线程的优先级
//mt1.setPriority(10);
//或者这样设置mt1的优先级
mt1.setPriority(Thread.MAX_PRIORITY);
mt2.setPriority(Thread.MIN_PRIORITY);
mt1.start();
mt2.start();
}
}
输出结果:可以发现,并不是高优先级的线程先执行完后低优先级的线程才执行,其任然存在一定的随机性;
换句话说,优先级高的程序并不一定会比优先级低的线程先运行;
线程线程1正在执行1
线程线程2正在执行1
线程线程1正在执行2
线程线程2正在执行2
线程线程1正在执行3
线程线程2正在执行3
线程线程1正在执行4
线程线程1正在执行5
线程线程1正在执行6
线程线程1正在执行7
线程线程1正在执行8
线程线程1正在执行9
线程线程1正在执行10
线程线程2正在执行4
线程线程2正在执行5
线程线程2正在执行6
线程线程2正在执行7
线程线程2正在执行8
线程线程2正在执行9
线程线程2正在执行10
线程同步
由前面几篇文章可知: 线程什么时候运行是不确定的 ,即:
●各个线程是通过竞争CPU的时间而获得运行机会的;
●各线程什么时候获得CPU时间,占用多久,是不可预测的;
●一个正在运行的线程在什么地方被暂停是不确定的;
●可能一个线程中一个方法(或一个代码块)刚执行到一半,这个线程就丧失了CPU的使用权,从而导致很多问题;
上面几条都透露着几个字:不确定。这个问题,在实际开发中也会带来不可预测的结果,问题;
……………………………………………………
如下面问题:银行账户的存款和取款。
(1)首先,存款和取款各是不同的线程在执行;
(2)存款操作和取款操作都是分很多步骤执行;
(3)不同的线程之间切换是随机的,即可能取款线程执行一半就停了,转去执行存款线程,而存款线程执行一般也可能暂停,转去执行取款线程;这种现象就会导致,最终的账户金额存在错误计算;
如下面代码:看个大概即可,程序很简单,主要是传达一种问题而已。
(1)Bank类,主要包括,存款方法和取款方法;(这儿用slee()方法,防止输出结果输出太快,看不清;而且实际中,一个方法的执行也需要一定的时间的);
public class Bank {
private String account;// 账号
private int balance;// 账户余额
public Bank(String account, int balance) {
this.account = account;
this.balance = balance;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Bank [账号:" + account + ", 余额:" + balance + "]";
}
// 存款
public void saveAccount() {
// 获取当前的账号余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 修改余额,存100元
balance += 100;
// 修改账户余额
setBalance(balance);
// 输出存款后的账户余额
System.out.println("存款后的账户余额为:" + balance);
}
public void drawAccount() {
// 在不同的位置处添加sleep方法
// 获得当前的帐户余额
int balance = getBalance();
// 修改余额,取200
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 修改帐户余额
setBalance(balance);
System.out.println("取款后的帐户余额:" + balance);
}
}
(2)取款类,做成了一个线程类(因为取款操作是一个单独的线程)
//取款
public class DrawAccount implements Runnable{
Bank bank;
public DrawAccount(Bank bank){
this.bank=bank;
}
@Override
public void run() {
bank.drawAccount();
}
}
(3)存款类,也做成了一个线程类
//存款
public class SaveAccount implements Runnable{
Bank bank;
public SaveAccount(Bank bank){
this.bank=bank;
}
public void run(){
bank.saveAccount();
}
}
(4)测试类
public class Test {
public static void main(String[] args) {
// 创建帐户,给定余额为1000
Bank bank=new Bank("1001",1000);
//创建线程对象
SaveAccount sa=new SaveAccount(bank);
DrawAccount da=new DrawAccount(bank);
Thread save=new Thread(sa);
Thread draw=new Thread(da);
save.start();
draw.start();
try {
draw.join();
save.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(bank);
}
}
解决办法:
●为了保证在存款或取款的时候,不允许其他线程对账户余额进行操作;需要将Bank对象进行锁定,即某个线程在操作Bank对象时,其他线程都不允许进行操作;
●使用关键字 synchronized(同步关键字)实现;该关键字,可以确保共享对象,在同一个时刻只能被一个线程访问,这种机制成为线程同步,或线程互斥。
● synchroniced具体的使用技巧,需要在以后的实际开发中逐步加深理解,逐步总结其应用场景,惯用应用技巧等。
synchroniced:
如上面Bank类改造如下:分别在存款方法和取款方法处,使用了synchroniced关键字。
如: saveAccount()方法添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的 。
public class Bank {
private String account;// 账号
private int balance;// 账户余额
public Bank(String account, int balance) {
this.account = account;
this.balance = balance;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Bank [账号:" + account + ", 余额:" + balance + "]";
}
// 存款
// saveAccount()方法添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的。
public synchronized void saveAccount() {
// 获取当前的账号余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 修改余额,存100元
balance += 100;
// 修改账户余额
setBalance(balance);
// 输出存款后的账户余额
System.out.println("存款后的账户余额为:" + balance);
}
public void drawAccount() {
// 语句块添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的。
// 因为当前语句块是针对Bank类的,即是针对Bank类型的对象的,所以直接this就行了
synchronized (this) {
// 在不同的位置处添加sleep方法
// 获得当前的帐户余额
int balance = getBalance();
// 修改余额,取200
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 修改帐户余额
setBalance(balance);
System.out.println("取款后的帐户余额:" + balance);
}
}
}
线程间通信
可以设想这样一种情况:取款时,账务余额不够了;顺理成章的解决方案是:等待存入足够的钱以后再进行取款操作。(这种业务场景很多,不要仅仅局限于银行存取款喽)
这儿涉及线程间通信。
如下示例:
业务情况:
Queue类:该类相当于一个容器,其有一个变量n;
Producer类:充当生产者线程,该类负责对变量n的值进行加1运算;
Consumer类:充当消费者线程,该类负责对变量n的值进行减1运算;
或者认为Producer类负责数据的生产,Consumer类负责数据的取用;
业务要求:生产一个消费一个。
……………………………………………………
1.首先,是没有添加线程间通信的情况:
Queue类:
public class Queue {
private int n;
public synchronized int getN() {
System.out.println("消费:"+n);
return n;
}
public synchronized void setN(int n) {
System.out.println("生产:"+n);
this.n = n;
}
}
Producer类,生产类:
// Queue类中包含 创造数据和获取数据两个方法(或称两个业务),若想让两个业务各自相对独立,需要涉及两个独立的线程类,分别对应两个业务的逻辑方法
// 这儿,演示意味浓重,在实际开发中,具体哪些逻辑代码需要设置成一个线程,需要慢慢累积经验
public class Producer implements Runnable{
Queue queue; // 定义一个Queue类型的变量,再实例化本线程类的时候,该变量会指向线程类实际指向的Queue对象
public Producer(){}
public Producer(Queue queue){
this.queue = queue;
}
@Override
public void run() {
int i = 0;
while(true){
queue.setN(i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Consumer类,消费类:
public class Consumer implements Runnable{
Queue queue; // 定义一个Queue类型的变量,再实例化本线程类的时候,该变量会指向线程类实际指向的Queue对象
public Consumer(){}
public Consumer(Queue queue){
this.queue = queue;
}
@Override
public void run() {
while(true){
queue.getN();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Test类,测试类:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Queue queue = new Queue();
// (1)这个例子,主要说明,在实际业务中,两个进程可以交叉运行,交叉运行的控制机制不能单靠sleep()等控制执行时间的方式;
// 如果两个线程是互相配合的情况,还需要两个线程互相交流一些数据,只有如此,才能在符合业务逻辑需求的情况下,正确地执行程序
// (2)下面两个线程,因为线程方法处添加了synchronized同步关键字,这可以保证,在一个方法执行完毕前该线程不会被中途打断;
// 但,如果当方法(本次)执行完毕,调用的sleep()时间过去后,下一次到底是执行生产方法还是消费方法,是随机的,并没有参考n的值到底是多少;(这个sleep()仅仅是
// 为了演示方便,拖长方法的执行时间,否则输出结果滑太快,看不清楚输出)
// (3)也可以发现,n的值是生产类初始化和控制的,消费类只是去获取n的值而已;按照逻辑是,生产类每生产一个,消费类紧接着就消费一个;但因为,生产类和消费
// 类之间到底该执行谁,没有一个可以完成上述业务需求的逻辑控制,所以会出现逻辑问题。
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
执行结果(只截取了前面几行):通过结果可以发现,第三行结果应该是生产1,即生产0,消费0之后,按照业务逻辑来说应该执行生产1,消费1…;但显然,第二行执行完后,生产和消费两个线程(在没有逻辑控制的)下一步该执行哪个是完全随机的,这种执行流程是不符合业务要求的。因此,有必要使得两个线程间能够彼此通信,从而使的线程间的执行顺序不再充满随机性,而是按照一定的逻辑执行;由此也是,线程间有点相互参照和依赖的味道。
生产:0
消费:0
消费:0
生产:1
消费:1
生产:2
生产:3
消费:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
生产:7
消费:7
消费:7
生产:8
2.其次,线程间通信:
●wait()方法:中断方法的执行,使线程等待;如果只有wait()方法,可能会出现:生产线程等待消费线程执行,消费线程等待生产线程执行,这样互相等待,谁都不会执行,就会处于死锁的状态;
●notify()方法:唤醒处于等待的某一个线程,使其结束等待;
●notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待;这两个方法和wait()方法对应使用;
sleep() 和 wait() 的这 5 个区别,你知道几个?
(1)只使用wait()方法,而没有使用notify()和notifyAll()的情况:
Queue类:该类添加了flag变量,并在方法中添加了wait()方法和控制逻辑,但没有使用notify()和notifyAll();
public class Queue {
private int n;
/**
* (不提倡再变量上用这种注释方式)
* flag:
* (1)flag初始值为false,flase代表Queue里面没有数据,此时不能消费数据,只能生产数据;
* (2)flag为true代表Queue里有数据,此时不能再生产数据了,只能消费数据,等到数据被消费了,flag编程false后才能去生产数据
*/
boolean flag = false;
public synchronized int getN() {
// 当flag为false的时候,没有数据,就无法消费,该方法不能执行,需要等待生产方法的执行;
if(!flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("消费:"+n);
// 消费完毕后,需要把falg设置称false,表示没有数据了
flag = false;
return n;
}
public synchronized void setN(int n) {
// 当flag为true的时候,有数据了,不能再生产了,需要等待消费方法的执行;数据被消费后,flag编程false后生产方法才能执行
if(flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("生产:"+n);
this.n = n;
// 数据生产完毕后,需要把falg设置称true,表示已经有数据了
flag = true;
}
}
执行结果:下面是所有的输出结果,发现,其实现了生产一个消费一个的逻辑;但当生产7后,并没有继续出现消费7,这就是出现了死锁的情况。
生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
解决办法:添加notifyAll();时刻不忘唤醒线程。( 注:至于为什么会出现死锁,暂时还不明白 )
public class Queue {
private int n;
/**
* (不提倡再变量上用这种注释方式)
* flag:
* (1)flag初始值为false,flase代表Queue里面没有数据,此时不能消费数据,只能生产数据;
* (2)flag为true代表Queue里有数据,此时不能再生产数据了,只能消费数据,等到数据被消费了,flag编程false后才能去生产数据
*/
boolean flag = false;
public synchronized int getN() {
// 当flag为false的时候,没有数据,就无法消费,该方法不能执行,需要等待生产方法的执行;
if(!flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("消费:"+n);
// 消费完毕后,需要把falg设置称false,表示没有数据了
flag = false;
// 唤醒线程
notifyAll();
return n;
}
public synchronized void setN(int n) {
// 当flag为true的时候,有数据了,不能再生产了,需要等待消费方法的执行;数据被消费后,flag编程false后生产方法才能执行
if(flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("生产:"+n);
this.n = n;
// 数据生产完毕后,需要把falg设置称true,表示已经有数据了
flag = true;
// 唤醒线程
notifyAll();
}
}
执行结果:只截取了部分,发现,程序的死锁情况解决了。
生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
消费:7
生产:8
消费:8
生产:9
消费:9
生产:10
消费:10
生产:11
消费:11
生产:12
消费:12
生产:13
消费:13
注: **(1) 具体线 程间通信,还需要通过实际业务来加深;这些可以应用哪些场景,应用时有哪些技巧等等都需要慢慢总结;
(2)线程间通信的场景也是千变万化,具体为了实现线程间通信,如何涉及逻辑代码,如何保证安全和效率的情况下的编写技巧和编写习惯做法,都需要慢慢积累;
Callable:创建线程的第三种方式
Callable接口是属于Executor框架(并发编程的一个框架)中的功能类,Callable接口和Runnable接口的功能相似,但功能比Runnable接口更强大。
(1)Callable接口可以在任务结束时提供一个返回值,Runnable接口没有这个功能;
(2)Callable接口的call()方法可以抛出异常,Runnable接口的run()方法不能抛出异常;
(3) Callable接口的深化,可等到接触并发编程的时候再了解 ;
Callable接口的示例程序如下:
创建Callable接口的实现类ThreadCallable,并实现call()方法;
/**
* 创建Callable接口的实现类,并实现call()方法,该call()方法作将为线程的执行体,有返回值
* @author Administrator
*
*/
public class ThreadCallable implements Callable<String>{
@Override
public String call() throws Exception {
// call()方法返回值的类型可以根据需求更改
System.out.println("sdfdsfdsfds");
String str = "创建线程的第三种方式";
return str;
}
}
实际调用:
public class CallableTest {
public static void main(String[] args) {
// (1)创建Callable接口实现类的实例对象;
Callable<String> call = new ThreadCallable();
// (2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值;
FutureTask<String> ft = new FutureTask<>(call);
// (3)使用FutureTask对象,作为Thread对象的target创建线程。(FutureTask类实现了Runnable接口)
Thread t = new Thread(ft);
// (4)启动线程
t.start();
// (5)调用FutureTask对象的get()方法获得子线程结束后的返回值
try{
System.out.println(ft.get());
}catch(ExecutionException e){
e.printStackTrace();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
输出结果:
sdfdsfdsfds
创建线程的第三种方式