多线程从入门到高级(1)---多线程基本使用

参考:《Java多线程编程核心技术》

一、程序、线程与进程

  • 程序:程序是存储在磁盘上, 包含可执行机器指令和数据的静态实体

  • 进程:

    1

上面是百度百科对进程这个词条的解释,程序是静态的,存储在磁盘上,进程是动态的,当程序运行起来就成了进程。

  • 线程:在百度百科对进程的解释中,已经说到了进程是线程的容器。线程是在进程中独立运行的子任务,这也说明了进程与线程是包含的关系

二、创建线程的两种方法

在Java的jdk中已经自带了对多线程技术的支持,实现多线程的方式主要有两种:

  • 继承Thread类
  • 实现Runnable接口
1. 继承Thread类
public class MyThreadTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread thread = new MyThread();
        thread.start();
        //thread.start();
        System.out.println("执行结束");
    }
}
class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        System.out.println("MyThread!!");
    }
}

2

创建一个类继承Thread,然后重写里面的run方法,在main方法中,通过Thread对象的start()方法去启动一个线程,通过执行结果,我们已经可以看到CPU是先执行MyThread类总的run方法的,可见多线程的执行顺序与代码的顺序没有关系!

2. 实现Runnable接口

java的单继承机制就决定了如果只有继承Thread的方法是具有局限性的,所以出现了实现Runnable接口的方法,同样是上面的代码思路

public class MyThreadTest {
    
    
    public static void main(String[] args) {
    
    

        Thread t1 = new Thread(new MyThread());
        t1.start();
        System.out.println("执行结束");
    }
}
class MyThread implements Runnable {
    
    

    @Override
    public void run() {
    
    
        System.out.println("MyThread!!");
    }
}

需要启动用实现Runnable方法的线程,可以将其作为参数传入Thread类中,下面是Thread类的构造方法

3

可见,Thread类的构造方法可以传入一个Runnable接口的实例,多线程的启动永远都是Thread类的start()方法

也可以使用匿名内部类、lamdba的方式去简洁的创建线程

public class MyThreadTest {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("MyThread!!");
            }
        });
        t1.start();
        System.out.println("执行结束");
    }
}
    public static void main(String[] args) {
    
    
        Runnable runnable = () -> System.out.println("MyThread!!");
        Thread t1 = new Thread(runnable);
        t1.start();
        System.out.println("执行结束");
    }
3.一些注意
  1. 要启动多线程,必须调用start方法,而不是run方法,调用run方法只是在主线程里顺序的执行
  2. 执行start方法的顺序,并不代表线程的先后执行顺序
public class MyThread extends Thread {
    
    
    private int i;
    public MyThread(int i){
    
    
        this.i = i;
    }

    @Override
    public void run() {
    
    
        System.out.println(i);
    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    
        MyThread t1 = new MyThread(1);
        MyThread t2 = new MyThread(2);
        MyThread t3 = new MyThread(3);
        MyThread t4 = new MyThread(4);
        MyThread t5 = new MyThread(5);
        MyThread t6 = new MyThread(6);
        MyThread t7 = new MyThread(7);
        MyThread t8 = new MyThread(8);
        MyThread t9 = new MyThread(9);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
        t7.start();
        t8.start();
        t9.start();
    }
}

执行后的顺序为

6

可见,多线程的执行顺序根调用顺序并无联系!

  1. 不能多次调用start方法
public class MyThreadTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread thread = new MyThread();
        thread.start();
        thread.start();
        System.out.println("执行结束");
    }
}
class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        System.out.println("MyThread!!");
    }
}

多次调用start()方法,会抛出IllegalThreadStateException异常,

4

  1. Thread类也实现了Runnable接口

5

这就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread的对象,这样可以将Thread对象中的run()方法交给其他线程调用,由 其他线程启动线程

三、多线程的常用方法

1.线程的命名与取得
  1. currentThread()

7

  1. setName()

    ​ 给线程设置名字

  2. getName()

    ​ 获取线程的名字

public class CountOperate extends Thread {
    
    
    public CountOperate() {
    
    
        System.out.println("CountOperate---begin");
        System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName());
        System.out.println("this.getName()=" + this.getName());
        System.out.println("CountOperate---end");
    }

    @Override
    public void run() {
    
    
        System.out.println("run---begin");
        System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName());
        System.out.println("this.getName()=" + this.getName());
        System.out.println("run---end");

    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    
        CountOperate countOperate = new CountOperate();
        Thread t1 = new Thread(countOperate);
        t1.setName("A");	//设置线程名字
        t1.start();			//启动线程
    }
}

运行结果:

8

上面的代码还可以证明一个结论:CountOperate的构造方法只在main线程里执行的!

2. 线程休眠(sleep方法)

sleep方法的作用是在指定的毫秒数内让当前“正在执行的线程”休眠一段时间,注意:如果调用sleep的线程上拥有该对象的锁,那么调用sleep不会释放锁

public class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        try {
    
    
            System.out.println("run threadName =" + this.currentThread().getName() + " begin =" + System.currentTimeMillis());
            Thread.sleep(2000);
            System.out.println("run threadName =" + this.currentThread().getName() + " end =" + System.currentTimeMillis());
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    
        MyThread thread = new MyThread();
        System.out.println("run threadName =" + Thread.currentThread().getName() + " begin =" + System.currentTimeMillis());
        thread.start();
        System.out.println("run threadName =" + Thread.currentThread().getName() + " end =" + System.currentTimeMillis());
    }
}

9

3.线程让步yield()方法

调用yiled方法,会让线程交出CPU方法,让CPU去执行其他的线程,更sleep方法类似,也不会释放锁,但是yield不能控制具体的交出CPU的时间,注意:调用yield方法并不会让线程进入堵塞状态,而是让线程重回就绪状态,也许当前线程刚调用yiled方法,交出了CPU的控制权,而下次CPU又选择了该线程执行

public class MyThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 3; i++) {
    
    
            Thread.yield();
            System.out.println("run threadName =" + Thread.currentThread().getName() + " i=" + i);
        }
    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    
        MyThread t1 = new MyThread();
        new Thread(t1).start();
        new Thread(t1).start();
        new Thread(t1).start();
    }
}

执行结果:

10

4.线程停止方法

在Java中,有3种方法可以终止正在运行的线程

  • 使用退出标志,是线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行停止线程,但是这个方法以及被抛弃,该方法不安全
  • 使用interrupt方法
  1. 使用退出标志
public class MyThread implements Runnable{
    
    
    private boolean flag;

    @Override
    public void run() {
    
    
        try {
    
    
            flag = true;
            int i  = 1;
            while(flag) {
    
    
                    Thread.sleep(1000);
                    System.out.println("这是第" + i + "运行线程"+ "线程名称为" + Thread.currentThread().getName());
                    i++;
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
    public void setFlag(boolean flag){
    
    
        this.flag = flag;
    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    

        try {
    
    
            MyThread t1 = new MyThread();
            new Thread(t1).start();
            Thread.sleep(2000);
            t1.setFlag(false);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

11

  1. 使用interrupt

使用interrupt并不会马上就停止操作,调用interrupt()方法只是在当前线程上中打了一个停止的标记,和上面例子差不多,只不过上面的例子是手动打上这个标记,而调用interrupt()后JVM帮我们把这个标记打上。那么就需要有一个可以判断当前线程标记是否停止的方法,有两种方法可以判断当前线程是否停止

  • this.isinterrupt:测试当前线程是否已经中断,执行后具有将状态标志置清除为false
  • this.interrupted:测试线程是否已经中断,不清除状态标志

仅仅使用interrupt()方法只是在当前线程打了一个停止的标记,并不是真的停止线程:

public class MyThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 500000; i++) {
    
    
            System.out.println("i= " + (i + 1));
        }
    }
}
public class Run {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }
}

12

  1. 使用异常或者return来停止线程
public class MyThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        try {
    
    
            for (int i = 0; i < 500000; i++) {
    
    
                if(Thread.interrupted()){
    
    
                    System.out.println("停止状态!!");
                    throw new InterruptedException();
                }
                System.out.println("i= " + (i + 1));
            }
            System.out.println("我在for循环下面");
        }catch (InterruptedException e){
    
    
            System.out.println("进入catch!");
        }

    }
}
public class Run {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }
}

13

5. 线程的优先级
  • setPriority

设置线程的优先级

  • getPriority

取得线程的优先级

线程的优先级用整形数字表示,一共有1~10这10个等级,jdk中有3个常量来预置定义的优先级,默认优先级是5

 /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
  1. 优先级具有继承性

例如A线程启动B线程,则B线程的优先级和A一样

//线程1
public class MyThread1 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("MyThread1 run priority = " + Thread.currentThread().getPriority());
        new Thread(new MyThread2()).start();
    }
}
//线程2
public class MyThread2 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("MyThread2 run priority = " + Thread.currentThread().getPriority());

    }
}
//主线程
public class Run {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("main thread begin priority= " + Thread.currentThread().getPriority());
        Thread.currentThread().setPriority(6);
        System.out.println("main thread end priority= " + Thread.currentThread().getPriority());
        new Thread(new MyThread1()).start();
    }
}

这里说的继承并不是说继承的那种关系,而是指如果A线程启动B线程,那么A,B线程的优先级,默认就一样,上面代码中,main线程启动MyThread1线程,MyThread1启动MyThread2线程,如果改变了main线程的优先级那么MyThread1,MyThread2的优先级都会改变

14

四、线程的状态

线程有五种转态分别是创建、就绪、运行、堵塞和死亡状态:

  • 创建:生成线程对象,并没有调用线程的start方法,此时线程处于创建状态
  • 就绪:调用了线程的start方法,该线程就进入到了就绪状态,但此时该线程还没有轮到线程调度,此时处于就绪转态
  • 运行:线程调度将该改线程设置为当前线程,此时线程进入到了运行状态,开始执行run方法里面的代码
  • 堵塞:线程在运行的过程中,被暂停了,一般是要获取资源就绪之后在进行,调用sleep也会使线程进入堵塞状态
  • 死亡:当一个线程的run方法执行完毕会进入到死亡状态

五、死锁

多线程高并发带来的好处是效率提高了,但是也随之带来一个致命的问题–死锁。死锁是指两个线程在抢夺资源时,互相拿着对方需要的资源导致的一种僵持的状态,若没有外力作用,这些进程都无法向前推进

1.死锁例子
class handleLock implements Runnable{
    
    
    private String lockA;
    private String lockB;


    public handleLock(String lockA, String lockB) {
    
    
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
    
    
        synchronized (lockA){
    
    
            System.out.println(Thread.currentThread().getName() + "持有" + lockA);
            try {
    
     TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
    
    e.printStackTrace();}
            synchronized (lockB){
    
    
                System.out.println(Thread.currentThread().getName() + "尝试获取" + lockB);
            }
        }
    }
}

public class DeadLockDemo {
    
    

    public static void main(String[] args) {
    
    
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new handleLock(lockA,lockB), "ThreadAAA").start();
        new Thread(new handleLock(lockB,lockA), "ThreadBBB").start();
    }
}

上述代码表示了两个线程在拥有自己锁的情况下还想拥有其他人锁,导致僵持下去,造成了死锁的问题

2. 如何定位

死锁问题造成后如何定位是一个问题,在jdk中提供了很多的工具来帮助我们分析java中的代码,定位死锁问题:

  • jps:查看java程序进程 jps -l
  • jstack:stack堆栈,打印对应的堆栈信息,就像是捕获异常

image-20210312180057379

使用jps -l查看当前程序的进程id为19528

image-20210312180235444

使用jstack 进程id打印出堆栈信息,可以直接看到最后面:‘

image-20210312180341315
那么死锁出现的位置已经相当明显,定位出死锁之后在进行代码的排查

猜你喜欢

转载自blog.csdn.net/weixin_44706647/article/details/114702432