Java基础4——多线程

Posted 特拉法尔加

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础4——多线程相关的知识,希望对你有一定的参考价值。

线程依赖于进程而存在

进程:正在运行的程序 是操作系统进行资源分配和调度的独立单位 每个进程都有自己的内存空间和系统资源

多进程的意义:单进程的计算机只能做一件事情 DOS窗口就是典型的单进程 多进程的计算机可以在一个时间段内执行多个任务

  单核CPU在某个时间点只能执行一件事情,事实上CPU一直在高效切换各个进程

线程:一个进程内可以执行多个任务,每个任务可以看成是一个线程,线程是程序(进程)的执行单元或执行路径,是程序使用CPU的最小单位

多线程的意义:提高应用程序的使用率

  程序的执行都是在抢CPU的执行权,多个进程是在抢这个资源,而其中一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权

  我们是不敢保证哪个线程能够在哪个时刻抢到,所以线程的执行有随机性

并行:逻辑上同时发生,指在某个时间段内同时运行多个程序

并发:物理上同时发生,指在某个时间点上同时运行多个程序

Java程序运行原理:由Java命令启动JVM,JVM启动就相当于一个进程,接着该进程创建了一个主线程去调用main方法

JVM虚拟机的启动是多线程的,因为垃圾回收线程也要先启动,否则可能会导致内存溢出,垃圾回收加上前面的主线程,所以最少是两个线程

由于线程是依赖进程存在的,所以我们要先创建一个进程出来,而进程是系统创建的,所以我们需要调用系统功能创建一个进程,Java是不能直接调用系统功能的,所以我们没有办法直接实现多线程程序,但是Java可以调用C/C++写好的程序来实现多线程,而C/C++是可以调用系统功能创建进程的,Java在此基础上封装并提供了一些类供我们使用

通过查看API,发现有两种方式实现多线程

方式一:继承Thread类,再重写run方法,创建实例,启动线程

  注:并不是类中的所有代码都需要被线程执行,而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run方法用来包含那些被线程执行的代码

public class MyThread extends Thread {
    public void run(){
        //一般来说,被线程执行的代码肯定是比较耗时的,所以我们用循环改进
        for(int i=0;i<10;i++){
            System.out.println(i);
        }
    }
}

public class MyThreadDemo{
    public static void main(String[] args){
        //创建线程对象
        //MyThread my=new MyThread();
        //启动线程
        //my.run();
        //调用run方法直接调用就相当于普通的方法调用,所以看起来像单线程的效果,要想看到多线程的效果就需要另一个方法start
        //start 第一步:使线程开始执行 第二步:Java虚拟机调用该线程的run方法
        //run和start的区别?
        //run 仅仅是封装被线程执行的代码,直接调用,是普通方法
        //start 首先启动了线程,然后再由JVM调用该线程的run方法
        MyThread my=new MyThread();
        my.start();
        //IllegalThreadStateException:同一个线程被调两次start时会发生这个异常
        MyThread my1=new MyThread();
        my1.start();
        //设置名字
        my.setName("111");
        my1.setName("222");
    }
}

以上是通过无参构造函数设置名字,我们还可以通过有参数构造方法设置线程的名字

public class MyThread extends Thread {
    public MyThread(){

    }
    public MyThread(String name){
        super(name);
    }
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(i);
        }
    }
}

public class MyThreadDemo{
    public static void main(String[] args){
        MyThread my1=new MyThread("111");
        MyThread my2=new MyThread("222");
        my1.start();
        my2.start();
    }
}

获取main方法所在的线程对象的名称:Thread.currentThread().getName() //currentThread顾名思义就是获取当前线程 main所在线程的名称就是main

线程调度

线程调度的两种模型:

分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片

抢占调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么回随机选择一个,优先级高的线程获取CPU的时间片相对多一些

Java使用的是抢占调度模型

线程优先级

public class ThreadPriority extends Thread{
    public void run(){
        for (int i=0;i<100;i++) {
            System.out.println(getName()+":"+i);//为什么直接写getName也可以?
        }
    }
}
//返回线程对象的优先级 getPriority()
//线程默认优先级是5
//更改线程优先级 setPriority(int) 改的时候如果优先级不在MIN_PRIORITY到MAX_PRIORITY范围内的话就会抛出IllegalArgumentException
//MIN_PRIORITY 1
//MAX_PRIORITY 10
//NORM_PRIORITY 5
//设置优先级之后,需多次运行才能发现较明显的效果
public class ThreadPriorityDemo{
    ThreadPriority tp1=new ThreadPriority();
    ThreadPriority tp2=new ThreadPriority();
    ThreadPriority tp3=new ThreadPriority();

    tp1.setName("111");
    tp2.setName("222");
    tp3.setName("333");

    System.out.println(tp1.getPriority());//5
    System.out.println(tp2.getPriority());//5
    System.out.println(tp3.getPriority());//5

    tp1.start();
    tp2.start();
    tp3.start();
}

线程休眠——sleep

public class ThreadSleep extends Thread{
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(getName()+":"+i+"日期:"+new Date());
            //此处只能try catch,不可以throws,因为ThreadSleep中的run是重写了父类的方法,父类没有throws Exception,所以不可以throws
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e)            {
                e.printStackTrace();
            }
        }
    }
}
class ThreadSleepDemo{
    public static void main(String[] args){
        ThreadSleep ts1=new ThreadSleep();
        ThreadSleep ts2=new ThreadSleep();
        ThreadSleep ts3=new ThreadSleep();

        ts1.setName("aaa");
        ts2.setName("bbb");
        ts3.setName("ccc");

        ts1.start();
        ts2.start();
        ts3.start();
    }
}

线程终止——join

//public final void join(){
//  throws InterruptedException等待线程终止    
//}
public class ThreadJoin extends Thread{
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(getName()+":"+i);
        }
    }
}
class ThreadJoinDemo{
    public static void main(String[] args){
        ThreadJoin ts1=new ThreadJoin();
        ThreadJoin ts2=new ThreadJoin();
        ThreadJoin ts3=new ThreadJoin();

        ts1.setName("aaa");
        ts2.setName("bbb");
        ts3.setName("ccc");

        ts1.start();//线程ts1执行完毕再执行ts2和ts3线程
        try{
            ts1.join();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        ts2.start();
        ts3.start();
    }
}

线程礼让——yield

//public final void yield(){
//  暂停当前正在执行的线程对象,并执行其他线程
//  可以让多个线程的执行更和谐一些,但是不能靠它保证每人一次,轮流执行
//}
public class ThreadYield extends Thread{
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(getName()+":"+i);
            Thread.yield();
        }
    }
}
class ThreadYieldDemo{
    public static void main(String[] args){
        ThreadYield ts1=new ThreadYield();
        ThreadYield ts2=new ThreadYield();

        ts1.setName("aaa");
        ts2.setName("bbb");

        ts1.start();
        ts2.start();
    }
}

线程守护——Daemon

//public final void setDaemon(boolean on) 将线程标记为守护线程或用户线程,当正在运行的线程都是守护线程时,Java虚拟机退出
//该方法必须在启动线程前调用
public class ThreadDaemon extends Thread{
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(getName()+":"+i);
        }
    }
}
class ThreadYieldDemo{
    public static void main(String[] args){
        ThreadDaemon ts1=new ThreadDaemon();
        ThreadDaemon ts2=new ThreadDaemon();

        ts1.setName("aaa");
        ts2.setName("bbb");

        //设置守护线程 如果线程ccc结束,那么线程aaa和线程bbb也会被杀死,但是在ccc结束后的一小段时间内aaa和bbb由于"惯性"还会存活着
        ts1.setDaemon(true);
        ts2.setDaemon(true);

        ts1.start();
        ts2.start();

        Thread.currentThread().setName("ccc");
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

线程中断——interrupt

//public final void stop():让线程停止,但是该方法已过时 该方法具有不安全性
//public void interrupt():中断线程,把线程状态终止,并抛出InterruptedException
public class ThreadStop extends Thread{
    public void run(){
        System.out.println("开始执行"+new Date());
        try{
            Thread.sleep(10000);
        }catch(InterruptedException e){
            System.out.println("线程被终止了")
        }        
        System.out.println("结束执行"+new Date());
    }
}
class ThreadYieldDemo{
    public static void main(String[] args){
        ThreadStop ts1=new ThreadStop();
        ts1.start();
        //如果超过3秒钟,还没有醒过来,就停止
        try{
            Thread.sleep(3000);
            ts1.insterrupt();
        }catch(InterruptedException e){
            System.out.println("线程被终止了")
        }
    }
}

线程生命周期

新建:创建线程对象

就绪:该线程有执行资格,但是没有抢到CPU执行权利

运行:有执行资格,有执行权利

  阻塞:由于一些操作,让线程处于了该状态,没有执行资格,没有执行权,而另一些操作可以把它激活,激活后处于就绪状态

死亡:线程对象变成垃圾,等待被回收

技术分享

线程实现方式二——实现Runnable接口

//实现Runnable接口
//步骤:
//  1 自定义类MyRunnable实现Runnable接口
//  2 重写run()方法
//  3 创建MyRunnable类的对象
//  4 创建Thread类的对象,并把第3步的对象作为构造参数传递
public class MyRunnable implements Runnable{
    public void run(){
        for(int i=0;i<100;i++){
            //由于实现接口的方式 就不能直接使用Thread类的方法了 但可以通过下面的方法间接使用
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
class MyRunnableDemo{
    public static void main(String[] args){
        MyRunnable my=new MyRunnable();

        //Thread t1=new Thread(my);
        //Thread t2=new Thread(my);

        //t1.setName("aaa");
        //t2.setName("bbb");

        //除了上面传统的线程创建及命名方法之外,还有下面的方法
        Thread t1=new Thread(my,"aaa");
        Thread t2=new Thread(my,"bbb");

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

两种方式区别

技术分享

 

电影院买票的案例

//电影票1 100张票,3个窗口,设计程序模拟售票模拟售票
public class SellTicket extends Thread{
    private static int tickets=100;//为了让多个线程对象共享这100张票,我们需要用静态修饰
    public void run(){
        while(true){
            if(tickets>0){
                System.out.println(getName()+"正在出售第"+(tickets--)+"张票");
            }
        }
    }
}
class SellTicketDemo{
    public static void main(String[] args){
        SellTicket st1=new SellTicket();
        SellTicket st2=new SellTicket();
        SellTicket st3=new SellTicket();

        st1.setName("窗口1");
        st2.setName("窗口2");
        st3.setName("窗口3");

        st1.start();
        st2.start();
        st3.start();
    }
}

实现方式二——通过实现Runnable接口(推荐)

public class SellTicket implements Runnable{
    private int tickets=100;
    public void run(){
        while(true){
            if(tickets>0){
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
            }
        }
    }
}
class SellTicketDemo{
    public static void main(String[] args){
        SellTicket st1=new SellTicket();

        Thread t1=new Thread(st1,"窗口1");
        Thread t2=new Thread(st1,"窗口2");
        Thread t3=new Thread(st1,"窗口3");

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

以上的程序从表面上看没什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延时的情况,所以在出售一张票以后,需要一点时间的延迟

//加了延迟后发现了问题:
//相同的票卖了多次
//出现了负数票
public class SellTicket implements Runnable{
    private int tickets=100;
    public void run(){
        while(true){
            if(tickets>0){
                //为了模拟更真实的场景,我们稍微停一下
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
                //CPU每一次执行必须是一个原子性的操作(最简单基本的操作) i++就不是原子性的操作,因为i++实际上先得将i加上1,再把i+1赋值给i
                //先记录以前的值
                //接着运行ticket-1,注意此时ticket的值还是100
                //然后输出"窗口1正在出售第100张票" 如果这时t2来了 将会继续输出 "窗口2正在出售第100张票"
                //再将ticket-1的值赋给ticket,这时ticket的值才是99
            }
        }
    }
}
class SellTicketDemo{
    public static void main(String[] args){
        SellTicket st1=new SellTicket();

        Thread t1=new Thread(st1,"窗口1");
        Thread t2=new Thread(st1,"窗口2");
        Thread t3=new Thread(st1,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
//加了延迟后发现了问题:
//相同的票卖了多次
//出现了负数票
public class SellTicket implements Runnable{
    private int tickets=100;
        public void run(){
        while(true){
            if(tickets>0){
                //为了模拟更真实的场景,我们稍微停一下
                try{
                    Thread.sleep(100);//假设这时t1 t2 t3都进来了,并且休息
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
                //窗口1正在出售第1张票,tickets=0
                //窗口1正在出售第0张票,tickets=-1
                //窗口1正在出售第-1张票,tickets=-2
                //由于随机性和延迟性,会出现负数
            }
        }
    }
}
class SellTicketDemo{
    public static void main(String[] args){
        SellTicket st1=new SellTicket();

        Thread t1=new Thread(st1,"窗口1");
        Thread t2=new Thread(st1,"窗口2");
        Thread t3=new Thread(st1,"窗口3");

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

以上问题解决方案:

/*
导致问题产生的原因:
    1、是否是多线程环境
    2、是否有共享数据
    3、是否有多条语句操作共享数据
    有这三个条件中的任何一个都可能会出问题
    
我们的程序中,窗口1 2 3共同卖票,所以有共享数据
    而且if(tickets>0)和tickets--这两句操作了共享数据
    
    条件1和条件2是无法改变的,只能考虑改变条件3
    思路:把多条语句操作共享数据的代码给包成一个整体,让某个线程执行的时候,别的线程不能执行
    为此,我们可以利用Java给我们提供的同步机制
    同步代码块的格式:
        synchronized(对象){
            需要同步的代码
        }
        对象:同步可以解决安全问题的根本原因就在于这个对象上,该对象如同锁的功能
            要求是多个线程必须是同一把锁
        把多条语句操作共享的代码包起来
    
*/
public class SellTicket implements Runnable{
    private int tickets=100;
    //创建锁对象,只需要记住在外面创建,再把obj写到synchronized里面就可以了,同步代码块的锁对象可以是任意对象
    private Object obj=new Object();
    public void run(){
        while(true){
            synchronized(obj){
                if(tickets>0){
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
                }
            }
        }
    }
}
class SellTicketDemo{
    public static void main(String[] args){
        SellTicket st1=new SellTicket();

        Thread t1=new Thread(st1,"窗口1");
        Thread t2=new Thread(st1,"窗口2");
        Thread t3=new Thread(st1,"窗口3");

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

同步方法的格式及锁对象问题

  把同步关键字加在方法上 

  同步方法是this

private synchronized void sellTicket(){
    if(tickets>0){
        try{
            Thread.sleep(100);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets--+"张票");
    }
}

静态方法及锁对象问题

静态方法的锁对象是类的字节码文件对象(反射会讲)

//线程安全的类
StringBuffer sb=new StringBuffer();
Vector<String> v=new Vector<String>();
HashTable<String,String> h=new HashTable<String,String>();

//Vector是线程安全的,但是即使我们需要线程安全,也不会使用Vector
//可以用Collections的静态方法synchronizedList
List<String> list=Collections.synchronizedList(new ArrayList<String>());

总结:锁的三种实现方式:同步代码块 同步方法 静态方法

package cn.itcast_11;

public class SellTicket implements Runnable {

    // 定义100张票
    private static int tickets = 100;

    // 定义同一把锁
    private Object obj = new Object();
    private Demo d = new Demo();

    private int x = 0;
    
    //同步代码块用obj做锁
//    @Override
//    public void run() {
//        while (true) {
//            synchronized (obj) {
//                if (tickets > 0) {
//                    try {
//                        Thread.sleep(100);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                    System.out.println(Thread.currentThread().getName()
//                            + "正在出售第" + (tickets--) + "张票 ");
//                }
//            }
//        }
//    }
    
    //同步代码块用任意对象做锁
//    @Override
//    public void run() {
//        while (true) {
//            synchronized (d) {
//                if (tickets > 0) {
//                    try {
//                        Thread.sleep(100);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                    System.out.println(Thread.currentThread().getName()
//                            + "正在出售第" + (tickets--) + "张票 ");
//                }
//            }
//        }
//    }
    
    @Override
    public void run() {
        while (true) {
            if(x%2==0){
                synchronized (SellTicket.class) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票 ");
                    }
                }
            }else {
//                synchronized (d) {
//                    if (tickets > 0) {
//                        try {
//                            Thread.sleep(100);
//                        } catch (InterruptedException e) {
//                            e.printStackTrace();
//                        }
//                        System.out.println(Thread.currentThread().getName()
//                                + "正在出售第" + (tickets--) + "张票 ");
//                    }
//                }
                
                sellTicket();
                
            }
            x++;
        }
    }

//    private void sellTicket() {
//        synchronized (d) {
//            if (tickets > 0) {
//            try {
//                    Thread.sleep(100);
//            } catch (InterruptedException e) {
//                    e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName()
//                        + "正在出售第" + (tickets--) + "张票 ");
//            }
//        }
//    }
    
    //如果一个方法一进去就看到了代码被同步了,那么我就再想能不能把这个同步加在方法上呢?
//     private synchronized void sellTicket() {
//            if (tickets > 0) {
//            try {
//                    Thread.sleep(100);
//            } catch (InterruptedException e) {
//                    e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName()
//                        + "正在出售第" + (tickets--) + "张票 ");
//            }
//    }
    
    private static synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                    Thread.sleep(100);
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

class Demo {
}

到目前为止,虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5+提供了一个新的锁对象Lock

//Lock:
//        void lock() 获取锁
//        void unlock() 释放锁
//Lock是接口,ReentrantLok是具体的实现类
public class SellTicketDemo(){
    public static void main(String[] args){
        SellTicket st=new SellTicket();
        
        Thread t1=new Thread(st,"窗口1");
        Thread t2=new Thread(st,"窗口2");
        Thread t3=new Thread(st,"窗口3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

public class SellTicket implements Runnable(){
    private int tickets=100;
    private Lock lock=new ReentrantLock();
    public void run(){
        while(true){
            try{
              //加锁
              lock.lock();
                if(tickets>0){
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e){
                        e.printStackTree();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
                }
            }
            finally{
                //释放锁
                lock.unlock();
            }
        }
    }
}

同步弊端:
  效率低
  如果出现了同步嵌套,就容易产生死锁问题

所谓死锁,即两个或两个以上的线程在执行的过程中,因争夺资源产生的一种相互等待的现象

public class MyLock{
    //创建两把锁
    public static final Object objA=new Object();
    public static final Object objB=new Object();
}
public class DieLock extends Thread{
    private boolean flag;
    public DieLock(boolean flag){
        this.flag=flag;
    }
    public void run(boolean flag){
        if(flag){
            synchronized(MyLock.objA){
                Systen.out.printLn("if objA");
                synchronized(MyLock.objB){
                    System.out.printLn("if objB");
                }
            }
        }else{
            synchronized(MyLock.objB){
                Systen.out.printLn("else objB");
                synchronized(MyLock.objA){
                    System.out.printLn("else objA");
                }
            }
        }
    }    
}
public class DieLockDemo{
    public static void main(String[] args){
        DieLock dl1=new DieLock(true);
        DieLock dl2=new DieLock(false);
        
        dl1.start();
        dl2.start();
    }
}

与卖票程序中几个窗口共享同一资源的情况不同,有一种情况是各个线程在消耗公共资源的同时公共资源还会由生产者不断提供,这就是生产-消费问题,其实就是线程间通信问题,即不同种类的线程间针对同一个资源的操作

/*
 * 分析:
 *         资源类:Student    
 *         设置学生数据:SetThread(生产者)
 *         获取学生数据:GetThread(消费者)
 *         测试类:StudentDemo
 * 
 * 问题1:按照思路写代码,发现数据每次都是:null---0
 * 原因:我们在每个线程中都创建了新的资源,而我们要求的时候设置和获取线程的资源应该是同一个
 * 如何实现呢?
 *         在外界把这个数据创建出来,通过构造方法传递给其他的类。
 * 
 * 问题2:为了数据的效果好一些,我加入了循环和判断,给出不同的值,这个时候产生了新的问题
 *         A:同一个数据出现多次
 *         B:姓名和年龄不匹配
 * 原因:
 *         A:同一个数据出现多次
 *             CPU的一点点时间片的执行权,就足够你执行很多次。
 *         B:姓名和年龄不匹配
 *             线程运行的随机性
 * 线程安全问题:
 *         A:是否是多线程环境        是
 *         B:是否有共享数据        是
 *         C:是否有多条语句操作共享数据    是
 * 解决方案:
 *         加锁。
 *         注意:
 *             A:不同种类的线程都要加锁。
 *             B:不同种类的线程加的锁必须是同一把。
 */
public class StudentDemo{
    public static void main(String[] args){
        Student s=new Student();
        
        SetThread st=new SetThread(s);
        GetThread st=new GetThread(s);
        
        Thread t1=new Thread(st);
        Thread t2=new Thread(gt);
        
        t1.start();
        t2.start();
    }
}
public class SetThread implements Runnable{
    private Student s;
    private int x=0;
    
    public SetThread(s){
        this.s=s;
    }    
    
    public void run(){
        while(true){
            //s是外界传进来的,正好是两个线程共享的对象,所以将它作为锁对象
            synchronized(s){
                if(x%2==0){
                    s.name="aaa";
                    s.age=10;
                }else{
                    s.name="bbb";//当走到这里 get线程抢到执行权 此时就会得到bbb 10岁的结果
                    s.age=11;
                }
                x++;
            }
        }
    }
}
public class GetThread implements Runnable{
    private Student s;
    
    public GetThread(s){
        this.s=s;
    }
    
    public void

以上是关于Java基础4——多线程的主要内容,如果未能解决你的问题,请参考以下文章

Java 多线程基础多线程的实现方式

Java 多线程基础多线程的实现方式

号称史上最全Java多线程与并发面试题总结—基础篇

Java程序设计多线程基础

Java程序设计多线程基础

java基础入门-多线程同步浅析-以银行转账为样例