java如何实现多线程(上)

300 阅读5分钟

一,如何实现多线程

在java中实现多线程有三种方式:

1.继承Thread类并重写run方法。

2.实现Runnable接口。

3.实现Callable接口。

下面是代码案例:

(1)继承Thread类并重写run方法实现多线程

通过继承Thread创建多线程类

package thread;

/**
 * 继承方式实现多线程
 */
public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i =0 ; i<30 ;i++){
            System.out.println("MyThread======="+i);
        }
    }
}

在测试类中创建主线程--->测试输出结果

package thread;

public class Test01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //开启多线程,自动调用从写的run方法
        myThread.start();

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

打印结果无规律,因为cpu会分配时间片,当线程同时运行时,线程之间会去抢夺时间片

image.png

几种常用获取线程名的方式

(1)通过父类Thread中的getName()方法可以获取线程名称,但是这个类必须为Thread类的子类

package thread;

/**
 * 继承方式实现多线程,同时拿到当前线程名称
 */
public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i =0 ; i<30 ;i++){
        //getname()为父类的方法,作用是获取到该线程的名称
            System.out.println(getName()+"======="+i);
        }
    }
}

(2)通过Thread中的静态方法currentThread获取当前线程,之后调用getName方法获取线程名,这个方式可以在任意处获取线程名称。

package thread;

/**
 * 使用Thread.currentThread().getName()方法获取线程名,可以在任意位置获取线程名
 */
public class Test01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //开启多线程,自动调用从写的run方法
        myThread.start();

        for (int i =0;i<30;i++){
            //Thread.currentThread().getName()方法,Thread类中的currentThread静态方法获取到当前线程,
            // getName()获取线程名,可以在任意处获取线程名
            System.out.println(Thread.currentThread().getName()+"====="+i);
        }
    }
}

几种常用的自定义线程名的方式

(1)通过setName的方式自定义线程名。

线程类

package thread;

/**
 * 继承方式实现多线程,同时拿到当前线程名称
 */
public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i =0 ; i<30 ;i++){
            System.out.println(getName()+"======="+i);
        }
    }
}

测试类中通过线程类.setName()自定义线程名称

package thread;

/**
 * 使用Thread.currentThread().getName()方法获取线程名,可以在任意位置获取线程名
 */
public class Test01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //通过setName的方式自定义线程名称
        myThread.setName("线程A");
        //开启多线程,自动调用从写的run方法
        myThread.start();

        for (int i =0;i<30;i++){
            //Thread.currentThread().getName()方法,Thread类中的currentThread静态方法获取到当前线程,
            // getName()获取线程名,可以在任意处获取线程名
            System.out.println(Thread.currentThread().getName()+"====="+i);
        }
    }
}

(2)通过构造函数更改线程名

通过自身的构造函数,将传进来的name赋值给父类线程名,从而使用构造函数改变线程名称

package thread02;


public class Tickets extends Thread{

    int tickets=100;//票数

//通过自身的构造函数,将传进来的name赋值给父类线程名,从而使用构造函数改变线程名称
    public Tickets(String name){
        super(name);
    }
    @Override
    public void run() {
        while (tickets>0){
            tickets--;
            System.out.println(getName()+"剩余票数为"+tickets);
        }
    }
}

创建四个线程同时进行,同时给每个线程不同的名称进行测试。

package thread02;

/**
 * 多线程实现卖票
 */
public class test {

    public static void main(String[] args) {


        Tickets A = new Tickets("窗口A");
        Tickets B = new Tickets("窗口B");
        Tickets C = new Tickets("窗口C");
        Tickets D = new Tickets("窗口D");

        A.start();
        B.start();
        C.start();
        D.start();
    }
}

(2)实现Runnable接口创建多线程类

通过实现Runable创建多线程任务类

实现了Runnable接口的类就是线程任务类,线程任务类需要使用线程对象才能运行

package thread03;

/**
 * 使用接口的方式实现多线程
 */
public class MyRunnable implements Runnable{

    @Override
    public void run() {
    //线程任务代码
        int i =100;
        while (i>0){
            i--;
            System.out.println(Thread.currentThread().getName()+"~~~~~~~~~~~~~~~"+i);
        }
    }
}

创建线程对象--->执行线程任务类对象

线程对象中可以放置两个参数,第一个参数为任务类对象,第二个参数为线程名称(可填可不填,不填就是系统默认值)

能使用实现接口的方式不要使用继承父类的方式,因为继承只支持单继承,扩展性比较差

package thread03;

public class test03 {

    public static void main(String[] args) {
        //线程任务类对象
        MyRunnable myRunnable = new MyRunnable();
        //创建线程对象,Runnable target
        Thread thread = new Thread(myRunnable,"线程A");
        /*运行线程对象*/
        thread.start();

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

    }
}

下面的例子为多个窗口共卖一张票,这种情况会出现超卖的现象和卖同一张票的情况。---这就是多线程操作公共数据时出现的线程安全问题=====后面的时候再解决。

package thread04;

public class SellingTickets implements Runnable{
        int tickets = 100;

    @Override
    public void run() {
        while (true){
            if(tickets>0){
                tickets--;
                System.out.println(Thread.currentThread().getName()+"买了一张票,还剩票数"+tickets);
            }else{
                break;
            }
        }
    }
}
package thread04;

public class test {

    public static void main(String[] args) {
        SellingTickets sellingTickets = new SellingTickets();
        Thread thread = new Thread(sellingTickets,"窗口1111");
        Thread thread2 = new Thread(sellingTickets,"窗口2222");
        Thread thread3 = new Thread(sellingTickets,"窗口3333");
        Thread thread4 = new Thread(sellingTickets,"窗口4444");
        thread.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

image.png

二,Thread类中常用的方法

(1)sleep线程休眠的用法及解释

package thread;

/**
 * 继承方式实现多线程,同时拿到当前线程名称
 */
public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i =0 ; i<30 ;i++){
            try {
                //使当前线程休眠xxx毫秒之后再执行
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+"======="+i);
        }
    }
}

image.png

(2)yiele线程放弃时间片的方法及解释

yiele线程放弃时间片,等待下一次竞争,此方法产生线程交替执行的概率比较高,但并不是百分百

package thread;

/**
 * 继承方式实现多线程,同时拿到当前线程名称
 */
public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i =0 ; i<30 ;i++){
                //使当前线程主动放弃时间片等待下一次竞争
                Thread.yield();
            System.out.println(getName()+"======="+i);
        }
    }
}
package thread;

/**
 * 使用Thread.currentThread().getName()方法获取线程名,可以在任意位置获取线程名
 */
public class Test01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("线程A");
        myThread.start();
        MyThread myThread2 = new MyThread();
        myThread2.setName("线程B");
        myThread2.start();
        
    }
}

image.png

image.png

(3)join线程加入的方法用法及解释

join方法,允许其他线程加载到当前线程中,直到其他线程执行完毕之后,当前主线程才会执行,如果有多个线程加入当前线程,那么加入的线程会进行抢夺时间片。

package thread;

/**
 * 使用Thread.currentThread().getName()方法获取线程名,可以在任意位置获取线程名
 */
public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.setName("线程A");
        myThread.start();
        MyThread myThread2 = new MyThread();
        myThread2.setName("线程B");
        myThread2.start();


        myThread.join();
        myThread2.join();
        for (int i =0;i<30;i++){
            //Thread.currentThread().getName()方法,Thread类中的currentThread静态方法获取到当前线程,
            // getName()获取线程名,可以在任意处获取线程名
            System.out.println(Thread.currentThread().getName()+"====="+i);
        }
    }
}

通过打印结果可以看出主线程最后执行,而加入的线程会进行抢夺时间片执行

image.png

(4)**.setPriority()**线程优先级的说明及解释

线程优先级分为1-10十个等级,等级越高说明获取时间片的概率越高,但是这并不是一定的。

image.png

(5).setDaemon(true) 守护线程的说明及解释

  线程对象.setDaemon(true);设置为守护线程。      
  线程有两类:用户线程(前台线程)和守护线程(后台线程)      
  如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。      
  垃圾回收线程属于守护线程。

image.png

三,线程安全的解决方法

这里的临界资源对象指的是用哪个对象当锁,调用同一个临界资源对象的方法,他们便会同步执行,排队执行,而不会发生超卖等现象。 原子操作指的是要么都成功,要么都失败

注意:每个对象都有一个互斥锁标记,用来分配给线程的。 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。 线程退出同步代码块时,会释放相应的互斥锁标记。也就是开锁

使用同步代码块

格式为:

synchronized(临界资源对象){//对临界资源解锁
//代码(原子操作)
}

示例:

package thread04;

public class SellingTickets implements Runnable{
        int tickets = 100;

    @Override
    public void run() {
        while (true){
            synchronized (this){//对临界资源对象加锁,也就是this就是锁,也可以为其他对象,这里的临界资源对象只能是对象,当多个方法执行时,他们如果是同一个锁,才会互相排队执行
                if(tickets>0){
                    tickets--;
                    System.out.println(Thread.currentThread().getName()+"买了一张票,还剩票数"+tickets);
                }else{
                    break;
                }

            }

        }
    }
}
package thread04;

public class test {

    public static void main(String[] args) {
        SellingTickets sellingTickets = new SellingTickets();
        Thread thread = new Thread(sellingTickets,"窗口1111");
        Thread thread2 = new Thread(sellingTickets,"窗口2222");
        thread.start();
        thread2.start();

    }
}

输出结果:

可以看到现在的票数不会继续发生超卖,重复卖等现象。

image.png

注意!!!锁不要直接加在方法上,除非是特殊情况,因为如果直接加在方法上就会导致锁只能被一个线程使用,线程抢到之后执行完毕才会开锁,具体案例如下

package thread04;

public  class  SellingTickets  implements  Runnable{
    private int tickets = 100;

    @Override
    public synchronized  void run() {
        while (true){
                if (tickets > 0) {
                    tickets--;
                    System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩票数" + tickets);
                } else {
                    break;
                }
            
        }
    }
}
package thread04;

public class test {

    public static void main(String[] args) {
        SellingTickets sellingTickets = new SellingTickets();
        Thread thread = new Thread(sellingTickets,"窗口1111");
        Thread thread2 = new Thread(sellingTickets,"窗口2222");
        Thread thread3 = new Thread(sellingTickets,"窗口3333");
        Thread thread4 = new Thread(sellingTickets,"窗口4444");
        thread.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

image.png