多线程练习题——来自于《疯狂Java》

题目描述

使用两个线程,一个线程负责打印1~52,另外一个线程负责打印a~z,使用多线程通信的相关知识,使最后的打印结果为12a34b56c……5152z

知识背景(还没写完,直接看回到问题)

线程和进程

  1. 进程:进程是程序在特定的数据上的一次执行过程,相比于程序储存在硬盘当中,进程是一个动态的概念;进程是操作系统进行资源分配和调度的最小单位,进程只有在得到所需的资源才能执行;各个进程之间彼此独立,资源不能共享。运行的一个程序就是进程,就比如现在浏览器在在显示我的文章,背后至少包括一个进程

  2. 线程:线程是比进程还要小的一段执行过程,是轻量级别的进程,一个进程通常包含多个线程,多个线程之间可以并行执行(在单个cpu的情况下,某一个时间点只有一个线程在cpu上执行,但是具体那个线程在执行取决于cpu的调度)也可以并发执行(在多cpu的环境下,某一个时间点可以有多个线程在执行),正因为线程具有如此特性,使用多线程可以显著的提高程序执行的效率;同一个进程的里面的线程可以共享公共资源。

上述概念通过如下示意图说明:
假设有如下一段程序:接受用户的输入的字符,将它们全部转化为小写字母,最后在控制台展示
多线程练习题——来自于《疯狂Java》_第1张图片
说明
在多线程模式下,当线程一在等待用户输入时,线程三可以将线程二处理的结果在控制台展示

线程生命周期

多线程练习题——来自于《疯狂Java》_第2张图片
书上讲了很多,通过下面一个具体的例子将说明线程的创建的几种方式和执行的方法

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
     演示线程创建的三种方式
     1. 继承Thread类
     2. 实现Runnable接口
     3. 实现Callable接口,通过FutureTask获取返回值

     什么是线程,和进程之间的差别
     线程的状态,为什么线程会被阻塞
     并发执行和并行执行
     设置线程的优先级
 */

class Thread1 extends Thread{

    private String name;

    /**
     * 1. 调用父类构造方法为线程命名
     * @param name
     */
    public Thread1(String name) {
        super(name);
    }
    
    /**
     * 2. 重写run方法,定义线程体,实现线程完成的任务,注意run方法是可以被阻塞的
     */
    @Override
    public void run() {
        
        //具体任务打印字符
        for (int i = 0; i < 100;i++) {
            System.out.println(getName()+"正在执行"+i);
        }
    }
}

/**
 * 实现Runnable接口作为new Thread(Runnable run)的参数
 */
class Thread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "正在执行"+i);
        }
    }
}

/**
 * 1. 实现Callable接口
 */
class Thread3 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int i=0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "正在执行"+i);
            if(i%6==0){
                //线程休眠,超过休眠时间重新进入就绪状态
                Thread.sleep(0,100);
                //唤醒其他线程,通知他们可以抢占资源
                //在thread3休眠期间,如果想要获得thread3的返回值,不应该唤醒主线程,会导致如下错误
                /*
                    java.util.concurrent.ExecutionException:
                    java.lang.IllegalMonitorStateException:
                    current thread is not owner
                 */
//                notifyAll();

            }
        }
        return i;
    }
}
public class FirstThread {

    public static void main(String[] args) {
        //2. 接受返回值
        FutureTask<Integer> task = new FutureTask<>(new Thread3());

        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName() + "正在执行"+i);
            if(i==20){
                //3. 实际上,task即FutureTask对象也是作为Thread的task
                new Thread(task,"线程三").start();
            }
            if(i>20){
                try {
                    //4. 获取返回值
                    System.out.println(task.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    System.out.println(Thread.currentThread().getName() + "");
                    e.printStackTrace();
                }
            }
        }

    }
}

线程同步

问题

问题的产生:使用多线程可以提高程序的执行效率,但是线程执行的顺序如果不加以控制,是无法预料的,取决于cpu的调度;另外一方面,当多个线程需要共同访问修改某一公共资源时,其中某一个线程虽然修改了公共资源,但是另外一个线程完全有可能访问到的还是公共资源修改以前的值,原因如下:

线程需要修改某一个公共资源需要如下步骤:

多线程练习题——来自于《疯狂Java》_第3张图片
上述过程可以使用一个简单的例子说明:

Account是一个简单的账户类,有两个线程DrawThread需要访问修改里面的balance变量,如果不加以控制,将产生上述的后果

Account.java

import lombok.AllArgsConstructor;

/**
    describe the props and methods of Account
 */
@AllArgsConstructor
public class Account {

    private String name;
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}

DrawThread.java

/**
 * implement the logic of drawing money
 */
public class DrawThread extends Thread {

    private String name;
    private Account account;
    private double money;

    public DrawThread(String name, Account account, double money) {
        super(name);
        this.account = account;
        this.money = money;
    }
	/**
     * 取钱逻辑
     */
    public void draw() {
        if (account.getBalance() < money) {
            System.out.println(getName() + " draw money failed");
        } else {
            System.out.println(getName() + " draw money successfully");
            account.setBalance(account.getBalance() - money);
        }
    }

    @Override
    public void run() {
        draw();
    }
}

Test.java

public class Test {

    public static void main(String[] args) {
        Account account = new Account("account1", 1234);
        new DrawThread("draw1",account,1234).start();
        new DrawThread("draw2",account,1234).start();
    }
}

运行上面程序将得到下面的结果

draw1 draw money successfully
draw2 draw money successfully
balance is 0.0
balance is -1234.0

分析

从最后的结果,我们可以推知实际执行的过程

  1. draw1率先被cpu调度,进行draw()方法的if判断,结果为真,打印信息
  2. 来不及修改balance,这个时候cpu调度了线程二,注意此时balance没有被draw1修改,所以draw()方法if判断为真,也打印信息
  3. 但是这个时候cpu重新调度draw1,修改了balance变量,balance变为0,draw1正常结束
  4. cpu调度draw2,修改balance为-1234.0

在实际过程中,上述过程是不允许的,经过分析,造成上述现象的原因是draw1线程访问balance时,draw2也可以访问,而不管draw1是否“同意”;反之亦然。因此能不能控制当一个线程访问某一个资源时,除非它自己主动放弃资源,或者正常结束等因其他因素停止执行以后,其他线程才能访问资源,这就是线程同步

解决

以后补了,没时间了

回到问题

分析

基本类
(1)定义类为Print,封装打印字符printChar方法和打印数字printNumber方法
(2)定义两个线程:PrintNumber(线程1)负责打印数字;PrintChar(线程2)负责打印字符

同步分析

  1. 确保PrintNumber首先执行
    在print类中定义信号量semaphore,初始值为true,当且仅当semaphore为真时,PrintNumber才能执行;对于PrintChar方法恰好相反。

每次PrintNumber执行时,只能打印两个数字,然后自己阻塞自己,也就是调用wait方法,同时还应当去唤醒PrintChar线程,问题来了:调用wait方法以后,当前线程还能执行notifyAll()方法吗?答案肯定是不能的,因此应当先唤醒其他线程,再阻塞自己

得到第一版代码

代码

class Print{

    private String name;
    private boolean semaphore;//shared variable,control the communication of  threads

    public Print(String name,boolean semaphore){

        this.name = name;
        this.semaphore=semaphore;
    }

    public synchronized void printNumber(){
        //只有semaphore为真时,才执行PrintNumber线程
        if (semaphore){

            for(int i=0;i<26;i++){
                notifyAll();//先唤醒其他线程,因为此时semaphore肯定为true,所以PrintChar不会执行
                System.out.print((2*i+1)+""+(2*i+2));
                semaphore=false;//修改信号量为false,确保下一次执行的不是PrintNumber线程
                try {
                    wait();//阻塞自己
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }

    public synchronized void printChar(){
        if(!semaphore){

            for (int i = 0; i < 26; i++) {
                notifyAll();//先唤醒其他线程,因为此时semaphore肯定为false,所以PrintNumber不会执行
                char index=(char)(i+97);//字符和数字之间转化
                System.out.print(index);
                semaphore=true;//修改信号量
                try {
                    wait();//阻塞自己
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class PrintNumber extends Thread {

    private Print print;
    private String name;

    public PrintNumber(Print print,String name){
        super(name);
        this.print = print;
    }

    @Override
    public void run() {
        print.printNumber();
    }
}

class PrintChar extends Thread {

    private Print print;
    private String name;

    public PrintChar(Print print,String name){
        super(name);
        this.print = print;
    }

    @Override
    public void run() {
        print.printChar();
    }
}
public class Problem1 {


    public static void main(String[] args) {
        Print print = new Print("print", true);
        //注入同一个Print对象,实现资源共享
        new PrintNumber(print,"printNumber").start();//打印数字
        new PrintChar(print,"printChar").start();//打印字符
    }
}

结果

在这里插入图片描述
最后是得到我们的结果,但是细心的读者肯定发现了这个程序还在执行,他是没有停止的(旁边正方形边框为红色)

新问题 为什么没有正常结束呢?

在编写多线程代码时,程序没结束很正常,就和刚学车车老是熄火一样,为什么呢?稍加分析就发现了问题:
PrintNumber最后一次执行完毕打印5152,就调用了wait方法,阻塞,需要PrintChar线程唤醒;然后就轮到PrintChar最后一次执行,虽然他调用了notifyAll()方法,企图唤醒PrintNumber,但此时semaphore仍为false,PrintNumber不会执行,继续往下执行,打印最后一个字符以后,自己阻塞自己,按照以往情况,接下来将执行PrintNumber,但是PrintNumber线程中此时i=26,已经退出了循环,没有执行notifyAll唤醒PrintChar线程,因此PrintChar线程一直处于阻塞状态,程序无法结束

解决

要人为的,手动的,暴力的结束PrintChar线程,在打印完最后一个字符以后,需要手动结束循环,使用如下语句:

if(i==25)break;

是加在wait()方法前面还是后面,别问,问就是没有好好看黑体加粗斜体字。

你可能感兴趣的:(多线程,java,开发语言,jvm)