JavaSE 多线程(1)

本文详细介绍了Java多线程的基本概念、创建方法、线程状态及控制手段,并深入探讨了多线程的安全问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程的概述

这里写图片描述

多线程具有随机性,谁抢到谁执行。

  1. 多线程并行和并发的区别
    -并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
    -并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
    -比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。
    -如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。
  2. Java程序运行原理和JVM的启动是多线程的吗?
    -A:Java程序运行原理
    Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。
    -B:JVM的启动是多线程的吗
    JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

创建线程的两种方法

创建线程的第一种方法

/*
如何在自定义的代码中,自定义一个线程呢?

通过对API的查找,java已经提供了对线程这类事物的描述。就是thread类。

创建线程的第一种方式:继承Thread类。
步骤:
1,定义类继承Thread
2,复写Thread类中的run方法
3,调用线程的start方法,该方法有两个作用
   (1)启动线程
   (2)调用run方法
*/

clsaa Demo extends Thread{
    public void run() {        //重写run方法
            System.out.println("demo run");  
    }
}


class  ThreadDemo{
    public static void main(String[] args)  {
        Demo d = new Demo();   //创建好一个线程
        d.start();            //d.run(); 仅仅是对象调用方法,而线程创建了却没有运行
    }
}

创建线程的第二种方法: 实现runnable接口
步骤:
1,定义类实现runnable接口
2,覆盖runnable接口中的run方法,将线程要运行的代码存放在该run方法中
3,通过Thread类建立线程对象
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
为什么要将Runnable接口的子类对象传递给Thread的构造函数
因为,自定义的run方法所属的对象是Runnable接口的子类对象
所以要让线程去执行指定对象的run方法,就必须明确该run方法所属的对象
5,调用Thread类的start方法开启线程并调用Runnable接口子类的run方法

public class Demo3_Thread {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();   //4,创建Runnable的子类对
        Thread t = new Thread(mr);          //5,将其当作参数传递给Thread的构造函数
        t.start();                          //6,开启线程

        for(int i = 0; i < 1000; i++) {
            System.out.println("bb");
        }
    }
}

class MyRunnable implements Runnable {      //1,定义一个类实现Runnable

    @Override
    public void run() {                     //2,重写run方法
        for(int i = 0; i < 1000; i++) {     //3,将要执行的代码写在run方法中
            System.out.println("aaaaaaaaaaaa");
        }
    }   
}

实现Runnable和继承方式有什么区别呢?

这里写图片描述
查看源码的区别:
* a.继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法
* b.实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法

实现Runnable好处:
(1)避免了单继承的局限性。在定义线程时,建议使用实现方式。 Runnable方式最为常用
(2)多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
继承Thread好处
(1)好处是:可以直接使用Thread类中的方法,代码简单
(2)弊端是:如果已经有了父类,就不能用这种方法

两种方式的区别:
继承Thread:线程代码存放Thread子类run方法中
实现Runnable:线程代码存放在接口子类的run方法中


匿名内部类实现线程的两种方式

  • 继承Thread类
        new Thread() {                                  //1,new 类(){}继承这个类
            public void run() {                         //2,重写run方法
                for(int i = 0; i < 3000; i++) {         //3,将要执行的代码,写在run方法中
                    System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
                }
            }
        }.start();
  • 实现Runnable接口
        new Thread(new Runnable(){                      //1,new 接口(){}实现这个接口
            public void run() {                         //2,重写run方法
                for(int i = 0; i < 3000; i++) {         //3,将要执行的代码,写在run方法中
                    System.out.println("bb");
                }
            }
        }).start(); 

获取线程的名字和设置线程的名字

原来线程都有自己默认的名称,Thread-编号 该编号从0开始。

clsaa Test extends Thread{
    Test(String name){
        super(name);   //super(name) 调用父类实参为String name的构造器且必须放在第一行。
    }
    public void run(){   
        this.setName("线程一")             //通过setName设置线程名称
        for(int x=0;x<60;x++){   
              System.out.println(Thread.currentThread().getName()+"run...."+x)      //也可以用this.getName()                          
        }
    }
}
/*
getName() ; 是获取线程名称
Thread.currentThread() ;获取当前进程对象

设置线程名称:setName 或者构造函数
*/

class  ThreadDemo{
    public static void main(String[] args) {
        Test t1 = new Test("线程二");      //通过构造函数设置线程名称
        Test t2 = new Test("线程三");
        t1.start();
        t2.start();
    }
}

线程运行状态

Java线程在某个时刻只能处于以下七个状态中的一个。
1.New(新创建),一个线程刚刚被创建出来,还没有调用start()方法;

2.Runnable(可运行状态),当线程对象调用了start()方法的时候,线程启动进入可运行的状态。

3.Running(运行状态),获取了时间片,运行run()方法中的代码

4.Blocked(阻塞状态),表示线程阻塞于锁

5.Waiting(等待状态),表示线程进入等待状态,需要其他线程做出一些特定动作

6.Timed_Waiting(超时等待)不同于Waiting,他是在指定时间内自行返回

7.Terminated(被终止),因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
这里写图片描述


休眠线程 sleep

Thread.sleep(毫秒,纳秒), 控制当前线程休眠若干毫秒1秒= 1000毫秒 1秒 = 1000 * 1000 * 1000纳秒

public class Demo3_Sleep {

    public static void main(String[] args) throws InterruptedException {
        new Thread() {
            public void run() {
                for(int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {  
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...aaaaaaaaaa");
                }
            }
        }.start();

        new Thread() {
            public void run() {
                for(int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...bb");
                }
            }
        }.start();
    }
}

输出结果:thread-1 和thread-0已每隔1秒的时间交替运行输出

守护线程

setDaemon(), 设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出
非守护线程就相当于象棋中的帅,守护线程就相当于车马象士。非守护线程一旦结束,守护线程也就结束。
比如QQ就是非守护线程,QQ窗口就是守护线程。当QQ一关闭,QQ窗口也会自动关闭

public class Demo3_Sleep {

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread() {
            public void run() {
                for(int i = 0; i < 3; i++) {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {  
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...aaaaaaaaaa");
                }
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                for(int i = 0; i < 100; i++) {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {  
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...bb");
                }
            }
        };

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

输出结果:将t2设置为守护线程,可以看到t1结束,t2没执行完也结束了。
这里写图片描述

加入线程

join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续
join(int), 可以等待指定的毫秒之后继续

final Thread t1 = new Thread() {                   //匿名内部类在使用它所在方法中的局部变量时,必须用final修饰
        public void run() {
            for(int i = 0; i < 50; i++) {
                System.out.println(getName() + "...aaaa");

            }
        }
    };

    Thread t2 = new Thread() {
        public void run() {
            for(int i = 0; i < 50; i++) {
                if(i == 2) {
                    try {
                        t1.join(30);                        //加入,有固定的时间,过了固定时间,继续交替执行                       
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }
                }
                System.out.println(getName() + "...bb");

            }
        }
    };

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

执行两次bb后,给aaaa执行30毫秒,再给bb执行
这里写图片描述

礼让线程

yield让出cpu,让其他线程执行

public class Demo6_Yield {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}

class MyThread extends Thread {
    public void run() {
        for(int i = 1; i <= 1000; i++) {
            if(i % 10 == 0) {                        //每当i是10的倍数就让出CPU
                Thread.yield();                      //让出CPU
            }
            System.out.println(getName() + "..." + i);
        }
    }
}

设置线程的优先级

setPriority()设置线程的优先级
这里写图片描述
1代表最低优先级
5代表默认优先级
10代表最高优先级

public class Demo7_Priority {

    public static void main(String[] args) {
        Thread t1 = new Thread(){
            public void run() {
                for(int i = 0; i < 100; i++) {
                    System.out.println(getName() + "...aaaaaaaaa" );
                }
            }
        };

        Thread t2 = new Thread(){
            public void run() {
                for(int i = 0; i < 100; i++) {
                    System.out.println(getName() + "...bb" );
                }
            }
        };

        //t1.setPriority(10);                   设置最大优先级
        //t2.setPriority(1);

        t1.setPriority(Thread.MIN_PRIORITY);        //设置最小的线程优先级
        t2.setPriority(Thread.MAX_PRIORITY);        //设置最大的线程优先级

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

多线程的安全问题 - 线程同步

再看这个售票程序

public class Demo1_Synchronized {

    /**
     * @param args
     * 同步代码块
     */
    public static void main(String[] args) {
        final Printer p = new Printer();

        new Thread() {
            public void run() {
                while(true) {
                    p.print1();
                }
            }
        }.start();

        new Thread() {
            public void run() {
                while(true) {
                    p.print2();
                }
            }
        }.start();
    }
}

class Printer {
    public void print1() {
            System.out.print("华");
            System.out.print("中");
            System.out.print("科");
            System.out.print("技");
            System.out.print("大");
            System.out.print("学");
            System.out.print("\r\n");   
    }

    public void print2() {                      
            System.out.print("软");
            System.out.print("件");
            System.out.print("工");
            System.out.print("程");
            System.out.print("\r\n");   
    }   
}

输出结果:
这里写图片描述
通过分析我们发现,打印出了很多错误。

多线程的运行出现了安全问题。

问题的原因在于:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。

解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 这样就能阻止两个线程对同一个资源进行并发访问。

JAVA对于多线程的安全问题提供了专业的解决方式。 就是 同步代码块

1.什么情况下需要同步
-当多线程并发, 有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.
- 如果两段代码是同步的, 那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码.

2.同步代码块
-使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
-多个同步代码块如果使用相同的锁对象, 那么他们就是同步的

synchronized(对象){
    需要被同步的代码
}

就好比你在上厕所的时候,把门锁上,只有你执行完了之后别人才能进来使用。

public class Demo1_Synchronized {
    /**
     * @param args
     * 同步代码块
     */
    public static void main(String[] args) {
        final Printer p = new Printer();

        new Thread() {
            public void run() {
                while(true) {
                    p.print1();
                }
            }
        }.start();

        new Thread() {
            public void run() {
                while(true) {
                    p.print2();
                }
            }
        }.start();
    }
}

class Printer {
    Demo d = new Demo();
    public void print1() {
        //synchronized(new Demo()) {                            
        synchronized(d) {    //同步锁,一个线程进来后就锁上,执行完才打开,另外的进程才能执,锁对象可以是任意的
            System.out.print("华");
            System.out.print("中");
            System.out.print("科");
            System.out.print("技");
            System.out.print("大");
            System.out.print("学");
            System.out.print("\r\n");
        }
    }

    public void print2() {
        //synchronized(new Demo()) {        //锁对象不能用匿名对象,因为匿名对象不是同一个对象
        synchronized(d) {         //上下两个锁必须一致才行
            System.out.print("软");
            System.out.print("件");
            System.out.print("工");
            System.out.print("程");
            System.out.print("\r\n");
        }
    }
}

class Demo{}

同步的前提:

1,必须要有两个或者两个以上的线程。
2,必须是多个线程使用同一个锁,这样才能保证安全。

必须保证同步中只能有一个线程在运行,
好处:解决了多线程的安全问题
坏处:多个线程都需要判断锁,比较消耗资源

同步方法
使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的
非静态同步方法
非静态同步函数的锁是:this
还是上面的程序

class Printer {
    Demo d = new Demo();
    public synchronized void print1() {
            System.out.print("华");
            System.out.print("中");
            System.out.print("科");
            System.out.print("技");
            System.out.print("大");
            System.out.print("学");
            System.out.print("\r\n");
        }
    }

    public void print2() {
       synchronized(this){
            System.out.print("软");
            System.out.print("件");
            System.out.print("工");
            System.out.print("程");
            System.out.print("\r\n");
        }
    }
}

静态同步方法
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不是this。因为静态方法中也不可以定义this
静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class 该对象的类型是class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象
类名.class

class Printer {
    Demo d = new Demo();
    public static synchronized void print1() {
            System.out.print("华");
            System.out.print("中");
            System.out.print("科");
            System.out.print("技");
            System.out.print("大");
            System.out.print("学");
            System.out.print("\r\n");
        }
    }

    public void print2() {
       synchronized(Printer2.class){
            System.out.print("软");
            System.out.print("件");
            System.out.print("工");
            System.out.print("程");
            System.out.print("\r\n");
        }
    }
}

多线程安全问题
这里模拟一个售票的例子来说明多线程里的一些安全问题


public class Demo3_Ticket {
    /**
     * 需求:铁路售票,一共100张,通过四个窗口卖完.
     */
    public static void main(String[] args) {
        new Ticket().start();
        new Ticket().start();
        new Ticket().start();
        new Ticket().start();
    }
}

class Ticket extends Thread {
    private static int ticket = 100;  //必须是静态的才是4个线程共享100张票
    //private static Object obj = new Object();     //如果用引用数据类型成员变量当作锁对象,必须是静态的,才是4个对象共享相同的成员变量
    public void run() {
        while(true) {
            synchronized(Ticket.class) {
                if(ticket <= 0) {
                    break;
                }
                try {
                    Thread.sleep(10);               //线程1睡,线程2睡,线程3睡,线程4睡
                } catch (InterruptedException e) {          
                    e.printStackTrace();
                }
                System.out.println(getName() + "...这是第" + ticket-- + "号票");
            }
        }
    }
}

火车售票例子用runnable接口实现

public class Demo4_Ticket {
    /**
     * @param args
     * 火车站卖票的例子用实现Runnable接口
     */
    public static void main(String[] args) {
        MyTicket mt = new MyTicket();
        new Thread(mt).start();
        new Thread(mt).start();
        new Thread(mt).start();
        new Thread(mt).start();

        /*Thread t1 = new Thread(mt);               //多次启动一个线程是非法的
        t1.start();
        t1.start();
        t1.start();
        t1.start();*/
    }
}

class MyTicket implements Runnable {
    private int tickets = 100; //这里不用定义成static,因为只之创建一个对象mt
    @Override
    public void run() {
        while(true) {
            synchronized(Ticket.class) {  //锁对象也可以用this,应为只创建一次对象,this就代表mt
                if(tickets <= 0) {
                    break;
                }
                try {
                    Thread.sleep(10);               //线程1睡,线程2睡,线程3睡,线程4睡
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "...这是第" + tickets-- + "号票");
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值