day19-多线程

  1. 线程的生命周期

  2. 线程安全问题的举例和解决措施

  3. 同步代码块处理实现Runnable的线程安全问题

  4. 同步代码块处理继承Thread类的线程安全问题

  5. 同步方法处理实现Runnable的线程安全问题

  6. 同步方法处理继承Thread类的线程安全问题

  7. 线程安全的懒汉式单例模式

  8. 死锁问题

  9. Lock锁方式解决线程安全问题

  10. 同步机制练习

  11. 线程通信的例题

  12. sleep()和wait()的异同

  13. 线程通信的生产者消费者例题

  14. 创建多线程的方式三:实现Callable接口

  15. 使用线程池的好处

  16. 创建多线程的方式四:使用线程池

 

1,线程的生命周期

    Java用Thread类的内部类State(枚举类)定义了线程的几种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

day19-多线程

    当调用start()创建一个线程后,该线程不会立即被执行,而是进入就绪状态,此时可能有其他之后创建的线程也进入了就绪状态,CPU选择一个线程执行,这个线程可能是先创建的,也可能是后创建的;运行时的线程可能会因为CPU调度而失去执行权,变为就绪状态,等待CPU下一次调度而再被执行;而如sleep()等操作会让正在执行的线程进入阻塞状态,此时CPU的执行权不可能轮到它,CPU只会选择就绪状态的线程,所以这是一种不同于就绪的状态,当线程结束阻塞就会回到就绪状态,此时它可以被CPU调度执行;所有线程的结局只有一个,就是执行完或者发生其他情况变为死亡的状态

day19-多线程

 

2,线程安全问题的举例和解决措施

    以取钱为例:在每次取钱的操作之前,都要先判断余额是否大于要取的钱。如果两个线程同时对一个余额进行取钱操作,当第一个线程执行时先判断,发现3000<2000,恰好此时CPU发生调度,该线程进入就绪状态,第二个线程被执行,发现3000<2000,然后取走了2000,轮到第一个线程接着上次的状态继续执行,也取走了2000,此时余额变为-1000。这种情况显然是不允许的

day19-多线程

    两个线程对两份不同的数据进行操作不会出现线程安全问题,只有对同一数据操作时可能会出现。在之前的卖票例子中、单例模式的懒汉式实现中都可能出现线程安全问题

 

3,同步代码块处理实现Runnable的线程安全问题

    线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作

package com.atguigu.java;

/**
 * 卖票过程中出现重票、错票的线程安全问题
 * 原因:某个线程在操作车票的过程中,操作尚未完成就切换到其他线程执行
 * 解决:当线程a操作ticket时,其他线程不能参与进来,直到a操作完,其他线程才可以操作ticket,
 *     这种情况即使a线程出现了阻塞也不能改变
 * Java中通过同步机制解决线程安全问题
 *     方式一:同步代码块
 *     synchronized(同步监视器) { // 同步监视器即锁,任何一个类的对象都可以充当锁,要求多个线程必须共用同一把锁
 *         // 需要被同步的代码    (操作共享数据的代码,共享数据是多个线程共同操作的变量,比如ticket)
 *     }
 *
 *     方式二:同步方法
 *
 * 同步的方式可以解决线程安全问题。但在执行同步代码时,只能有一个线程参与,其他线程必须等待,这相当于是
 *     单线程执行,效率变低
 */

public class WindowTest1 {

    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window1 implements  Runnable {
    private int ticket = 100;
    Object obj = new Object(); // 只有一个Window1对象,也就只有一个Object对象

    @Override
    public void run() {
        // Object obj = new Object(); // 此时每个线程执行run()时都会创建一个Object对象,也就不是共用同一把锁了,所以会出现线程安全问题
        while(true) {
            synchronized(obj) { // 不加这一行时,就会出现下面的重票、错票情况
            // synchronized(this) // this表示唯一的那个Window1对象,所以这样写也是可以的
                if (ticket > 0) { // 当某个线程刚判断完就轮到另一个线程执行,就可能出现错票,比如出现为0或-1的票
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); // 当某个线程刚打印完,就轮到另一个线程执行,就可能出现重票
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

 

4,同步代码块处理继承Thread类的线程安全问题

package com.atguigu.java;

/**
 * synchronized包含的代码既不能多也不能少,如果还有操作共享数据的代码没有包含,则可能
 *     出现线程安全问题,如果包含多了,比如这个例子中将while循环包含进去,则会出现一个
 *     线程执行完所有的操作,其他线程根本得不到执行机会
 */

public class WindowTest2 {

    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window2 extends Thread {

    private static int ticket = 100;
    private static Object obj = new Object(); // 创建了三个Window2对象,所以要将obj定义为static以保证只有一个对象

    @Override
    public void run() {
        while(true) {
            synchronized(Window2.class) { // 正确的,说明类也是对象,且这个类只加载一次,只有一个对象。Class clazz = Window2.class,将一个类赋值给类类型的引用变量
            // synchronized(this) // 错误的,此时不同的Window2对象调用run(),this代表不同的对象
            // synchronized(obj) //正确的,Object对象只有一个
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

 

5,同步方法处理实现Runnable的线程安全问题

package com.atguigu.java;

/**
 * Java同步机制解决线程安全问题的方式之二:同步方法
 *     将操作共享数据的代码放在一个方法中,并将此方法声明为同步的
 */

public class WindowTest3 {

    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window3 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while(true) {
             show(); // 调用同步方法,也能解决线程安全问题
        }
    }

    private synchronized void show() { // 同步方法,也有同步监视器,要求是唯一的,默认为this
        // synchronized(this) { // 同步代码块的方式
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
        // }
    }
}

 

6,同步方法处理继承Thread类的线程安全问题

package com.atguigu.java;

public class WindowTest4 {

    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while(true) {
             show();
        }
    }

    private static synchronized void show() { // 同步监视器:Window4.class即当前类本身
        // private synchronized void show() {} // 错误的,此时同步监视器是this,本例中就是三个Window4对象
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); // 在静态方法中不能调用非静态结构
            ticket--;
        }
    }
}

 

7,线程安全的懒汉式单例模式

package com.atguigu.java1;

/**
 * 采用同步方法或同步代码块的方式
 */
 
public class BankTest {
    
}

// 懒汉式单例模式
class Bank {

    private Bank() {}

    private static Bank instance = null;

    // public static synchronized Bank getInstance() // 同步方法,同步监视器为Bank.class
    public static Bank getInstance() { // 如果两个线程都通过run()调用getInstance(),第一个线程在判断instance为null时
                                    // 发生调度,第二个线程调用该方法并创建了对象,回到第一个线程它又会创建一个对象

        // 方式一:效率较差,当创建了单例对象,之后的线程调用该方法都要先判断instance是否为null
//        synchronized(Bank.class) { // 同步代码块
//            if (instance == null) {
//                instance = new Bank();
//            }
//            return instance;
//        }

        // 方式二:效率较高
        if(instance == null) {
            synchronized(Bank.class) {
                if(instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

 

8,死锁问题

day19-多线程

package com.atguigu.java1;

public class ThreadTest {

    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread() { // 通过Thread类的匿名子类的匿名对象调用start()开启一个线程执行重写的run()
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100); // 增加发生死锁的概率,当开启的第一条线程执行并拿到s1锁后进入阻塞,
                                                // 此时开启的第二天线程执行并拿到s2锁,就形成了接下来二者都想
                                                // 拿到对方拥有的锁的局面,就形成了死锁,此时程序无法结束
                    } 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() { // 通过向Thread类传递一个实现Runnable接口的匿名实现类的匿名对象创建
                                    // Thread类的匿名对象,调用start()开启一个线程并执行重写后的run()
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }) {
        }.start();
    }
}
    另一个例子,有时候可能发生死锁的地方比较隐蔽,程序执行没问题不一定代表不可能发生死锁,应尽量避免死锁的产生

package com.atguigu.java1;
//死锁的演示
class A {
	public synchronized void foo(B b) { //同步监视器:A类的对象:a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步监视器:A类的对象:a
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {//同步监视器:b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步监视器:b
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();


		dl.init();
	}
}

 

9,Lock锁方式解决线程安全问题

day19-多线程

package com.atguigu.java1;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 解决线程安全的方式三:Lock锁
 *     1,实例化ReentrantLock
 *     2,将可能出现线程安全的代码放在try{}中,并调用ReentrantLock对象的lock方法
 *     3,在finally中调用ReentrantLock对象的unlock方法
 *
 * 面试题:synchronized和Lock的异同
 *     相同点:都可以解决线程安全问题
 *     不同点:synchronized机制(同步代码块或同步方法)在执行完相应的代码后,自动释放同步监视器,
 *         Lock则需要手动启动同步、手动结束同步
 * 面试题:解决线程安全问题的方式有哪些
 *     synchronized机制(同步方法和同步代码块)、Lock锁
 */

public class LockTest {

    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window implements Runnable {

    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock(); // 如果是继承Thread的方式,就要加上static,避免每个对象都有一把锁

    @Override
    public void run() {
        while(true) {
            try {
                lock.lock(); // 线程执行这一步后就获得了锁,在执行之后的代码时不会切换为其他线程,除非它释放了这把锁

                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally { // 无论什么情况,都会执行
                lock.unlock(); // 释放锁
            }
        }
    }
}

day19-多线程

 

10,同步机制练习

package com.atguigu.exer;

public class AccountTest {

    public static void main(String[] args) {
        Account acct = new Account(0);

        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

class Account {

    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    public synchronized void deposit(double amt) { // 同步监视器使用唯一的Account对象
    // public void deposit(double amt) // 会出现线程安全问题
        if(amt > 0) {
            balance += amt;

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance);
        }
    }
}

class Customer extends Thread {

    private Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for(int i = 0 ; i < 3 ; i++) {
            acct.deposit(1000);
        }
    }
}

 

11,线程通信的例题

package com.atguigu.java2;

/**
 * 两个线程交替打印1-100
 * 这个过程是:
 *     一开始线程一拿到锁(同步监视器),执行notify()没有影响,调用sleep()使自己进入阻塞状态但并不会切换
 *         线程执行,因为锁还是线程一的,执行完对共享数据的操作后,wait()使线程一进入阻塞,并且释放了锁
 *     接下来一定是线程二被执行,它先拿到锁,执行notify()唤醒了线程一,因为线程二拿着锁,所以不会切换到
 *         线程一,同样线程二执行sleep()进入阻塞,然后执行对共享数据的操作,接着执行wait()使自己进入
 *         阻塞并释放了锁。二者如此交替执行
 * 三个和线程通信相关的方法
 *     wait():执行此方法,该线程就会进入阻塞状态,并释放同步监视器
 *     notify():执行此方法,该线程就会唤醒一个wait()过的阻塞线程;如果有多个线程执行过wait(),则唤醒优先级最高的那个
 *     notifyALL():执行此方法,该线程唤醒所有wait()过的阻塞线程
 * 注意点:
 *     1,三个方法必须放在同步代码块或同步方法中(不能放在Lock中)
 *     2,三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则出现异常,这也是为什么三个方法不能放在
 *         Lock中的原因,因为Lock中没有同步监视器
 *     3,三个方法是定义在Object类中的
 */

class Number implements Runnable {

    private int number = 1;
    private Object obj = new Object();

    @Override
    public void run() {
        while(true) {
            synchronized (this) {
            // synchronized(obj)
                notify(); // 唤醒另一个阻塞的线程,notifyAll()唤醒所有阻塞的线程。相当于this.notify(),
                        // 同步监视器即为调用者,所以没报异常;如果同步监视器变为obj则会报异常
                // obj.notify(); // 正确写法

                if (number <= 100) {

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try { // 使得调用wait()的线程进入阻塞状态,不同于sleep(),调用wait()的线程会释放
                        // 锁(同步监视器)
                        wait(); // this.wait(); // 同上notify()
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                } else {
                    break;
                }
            }
        }
    }
}

public class CommunicationTest {

    public static void main(String[] args) {
        Number number = new Number();

        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程一");
        t2.setName("线程二");

        t1.start();
        t2.start();
    }
}

 

12,sleep()和wait()的异同

    面试题:         相同点:调用这两个方法都会使当前线程进入阻塞状态         不同点:1,声明位置不同,sleep()声明在Thread类中,wait()声明在Object类中                        2,调用的要求不同,sleep()可以在任何场景下调用,wait()必须在同步代码块或同步方法中调用                        3,是否会释放同步监视器,如果都在同步代码块或同步方法中被调用,sleep()不会释放锁,wait()会释放锁  

13,线程通信的生产者消费者例题

day19-多线程

package com.atguigu.java2;

/**
 * 是多线程问题:生产者线程和消费者线程
 * 有共享数据:店员或产品
 * 涉及到线程通信的问题
 */

public class ProductTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");

        p1.start();
        c1.start();
    }
}

class Clerk {

    private int productCount = 0;

    public synchronized void produceProduct() { // 生产产品的方法。唯一的Clerk对象作为同步监视器
        if(productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");

            notify(); // 从生产第一个产品开始就唤醒消费的线程。如果不加notify()就会出现这样的情况,当两个线程
                    // 都开启后,消费者线程先判断没有可消费的,就wait()进入阻塞,接着生产者执行,生产到20时
                    // 执行wait()进入阻塞
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void consumeProduct() { // 消费产品的方法
        if(productCount > 0) {
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;

            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread { // 生产者

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品...");

        while(true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.produceProduct();
        }
    }
}

class Consumer extends Thread { // 消费者

    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品...");

        while(true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consumeProduct();
        }
    }
}

 

14,创建多线程的方式三:实现Callable接口

day19-多线程

day19-多线程

package com.atguigu.java2;

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

/**
 * JDK5.0新增的方式
 * 1,创建一个实现Callable接口的实现类,并实现call(),将此线程要执行的操作写在call()中
 * 2,创建Callable接口实现类的对象,并将此对象作为参数传递给FutureTask类的构造器以创建FutureTask类的对象
 * 3,将上面FutureTask类的对象作为参数传递给Thread类的构造器,并创建Thread类的对象,然后调用start()开启一个线程
 * 4,如果需要call()的返回值,通过FutureTask对象的get()就可以拿到
 * 相较于Runnable方式创建线程,Callable方式更强大的点
 *     1,call()可以有返回值
 *     2,call()可以抛出异常并被外面捕获
 *     3,Callable支持泛型
 */

public class ThreadNew {

    public static void main(String[] args) {
        NumThread numThread = new NumThread();

        FutureTask futureTask = new FutureTask(numThread);

        new Thread(futureTask).start();

        try {
            Object sum = futureTask.get(); // get()可以拿到call()的返回值
            System.out.println("总和为:" + sum);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumThread implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 1 ; i <= 100 ; i++) {
            if(i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum; // 自动装箱,Integer类型
    }
}

 

15,使用线程池的好处

day19-多线程

 

16,创建多线程的方式四:使用线程池

day19-多线程

package com.atguigu.java2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 开发中一般都是使用线程池创建线程
 * 1,创建指定线程数量的线程池
 * 2,要创建一个线程并执行指定操作,需要提供实现Runnable接口或Callable接口实现类的对象
 * 3,关闭线程池
 * 面试题:创建多线程有几种方式
 */

public class ThreadPool {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10); // 创建一个大小为10的线程池,
                                                                // 该线程池是一个ExecutorService接口实现类的对象
        System.out.println(service.getClass()); // 查看service的实现类,ThreadPoolExecutor
//        ThreadPoolExecutor service1 = (ThreadPoolExecutor)service; // 使用下面两个方法需要强转为实现类类型,因为接口中没有这些方法
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        service.execute(new NumberThread()); // 开启一个线程
        service.execute(new NumberThread1()); // 只能传入一个实现了Runnable接口的类的对象,该类实现的是Run(),线程执行的也是run()
        // service.submit(); // 可以传入一个实现了Callable接口的类的对象,该类实现的是Call(),可以有返回值,submit()也返回该值
        service.shutdown(); // 用完后关闭线程池
    }
}

class NumberThread 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 NumberThread1 implements Runnable {
    @Override
    public void run() {
        for(int i = 0 ; i < 100 ; i++) {
            if(i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

上一篇:day19面向对象基本用法_ 好处和应用场景_面向对象的三大特性


下一篇:day19