码迷,mamicode.com
首页 > 编程语言 > 详细

java中的多线程

时间:2020-06-07 09:15:35      阅读:56      评论:0      收藏:0      [点我收藏+]

标签:安全   exception   产生   默认   方法区   src   描述   底部   demo   

1、多线程概述
2、启动线程的方式
3、线程生命周期
4、线程的一些方法
5、线程调度
6、线程的同步和异步
7、synchronized
8、死锁

多线程概述

  • 概述
    1、什么是进程?
    进程是一个应用程序(1个进程是一个软件)。
    2、什么是线程?
    线程是一个进程中的执行场景/执行单元。
    3、一个进程可以启动多个线程。
    例子:
    对于java程序来说,当在DOS命令窗口中输入,java HelloWorld 回车之后会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
    4、进程之间内存独立不共享。线程之间堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。
    5、多线程
    假设启动10个线程,会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行,这就是多线程。
    多线程并发可以提高程序的处理效率。(车站的多窗口购票)
    6、main方法结束只代表主线程结束了,其它线程可能还在执行。
    7、真正的多线程并发
    两个线程同时执行,互不影响。
    单核CPU不能做到真正的多线程并发,只是CPU处理速度极快,多个线程之间频繁切换执行,感觉起来是并发的。(多核就能实现真正的并发)

启动线程的方式

  • 第一种
    编写一个类,直接继承java.lang.Thread,重写run方法。
public class Demo{
    public static void main(String[] args) {
        //这里属于主线程,在主栈中运行
        //新建一个分支线程对象
        Thread thread = new MyThread();
        /*直接调用run方法,不会启动线程,
        不会分配新的分支栈(单线程)*/
        //thread.run();
        //启动线程
        thread.start();
        //下面的代码还是运行在主线程中
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程---》" + i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)。
        for (int i = 0; i < 5; i++) {
            System.out.println("线程分支---》" + i);
        }
    }
}

输出:
技术图片
输出结果中主线程和分支线程有多有少、有先有后是为什么?
创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态。所以输出结果有多有少、有先有后。

  • run和start的区别
    1、run只是在主线程里面调用了run方法,没有开辟新的栈空间,还是属于单线程。
    2、start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开出来, start()方法就结束了。
    线程就启动成功,启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的底部。 run和main是平级的(并发)。
  • 第二种
    编写一个类,实现java.lang.Runnable接口,实现run方法。
public class Demo{
    public static void main(String[] args) {
        /*创建一个可运行的对象*/
        //Runnable mr = new MyRunnable();
        /*将可运行的对象封装成一个线程对象*/
        //Thread thread = new Thread(mr);
        /*合并代码*/
        Thread thread = new Thread(new MyRunnable());
        //启动线程
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程---》" + i);
        }
    }
}

/**这并不是一个线程类,是一个可运行的类。它还不是一个线程。*/
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("分支线程---》" + i);
        }
    }
}

输出(部分):
技术图片
优先选择第二种。第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活

  • 采用匿名内部类的方式
public class Demo{
    public static void main(String[] args) {
        /*匿名内部类*/
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("分支线程---》" + i);
                }
            }
        });
        //启动线程
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程---》" + i);
        }
    }
}

输出(部分):
技术图片

线程生命周期

  • 图例
    技术图片
  • 线程对象的生命周期
    新建状态
    就绪状态
    运行状态
    阻塞状态
    死亡状态

线程的一些方法

关于线程的名字

1、修改线程对象的名字:
void setName(String name)
将此线程的名称更改为参数 name 。

线程对象.setName("线程名字");

2、获取线程对象的名字:
String getName()
返回此线程的名称。

String name = 线程对象.getName();

3、当线程没有设置名字的时候,默认的名字规律:
Thread-0
Thread-1
Thread-2
...
4、代码示例

public class Demo{
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.setName("t1");
        System.out.println(myThread1.getName());

        MyThread myThread3 = new MyThread();
        System.out.println(myThread3.getName());

        MyThread myThread4 = new MyThread();
        System.out.println(myThread4.getName());

        myThread1.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("分支线程---》" + i);
        }
    }
}

输出:
技术图片

获取当前线程对象

1、static Thread currentThread()
返回对当前正在执行的线程对象的引用。

public class Demo{
    public static void main(String[] args) {
        /*currentThread就是当前线程对象,
        这个代码出现在main方法当中,所以当前线程就是主线程。*/
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName());
        //主线程名字就叫main

        MyThread myThread1 = new MyThread();
        myThread1.setName("t1");
        System.out.println(myThread1.getName());
        myThread1.start();

        MyThread myThread2 = new MyThread();
        myThread2.setName("t2");
        System.out.println(myThread2.getName());
        myThread2.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            /*currentThread就是当前线程对象。
            谁执行run方法,当前线程就是谁。*/
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName() + "线程---》" + i);
        }
    }
}

输出:
技术图片

sleep方法

1、static void sleep(long millis)
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
static void sleep(long millis, int nanos)
导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
(放弃占有CPU时间片,让给其它线程使用。)
2、可以实现这样一种功能:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
3、代码示例

public class Demo{
    public static void main(String[] args) {
        //让当前线程休眠5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello kitty");
        //5秒后才会输出hello kitty
    }
}

4、sleep是一个静态方法,无论前面引用是什么(???.sleep)都会转换成Thread.sleep,并且只会使当前正在执行的线程休眠。

interrupt方法

1、void interrupt()
中断这个线程。
2、实际不是中断线程的执行,是终止线程的阻塞状态。这种中断阻塞状态的方式依靠了java的异常处理机制。interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常。
3、例如
线程通过sleep()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
4、代码示例

public class Demo{
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();
        //睡眠5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中段t线程的睡眠
        t.interrupt();
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->begin");
        System.out.println("5秒后输出以下信息:");
        try {
            Thread.sleep(1000*60);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("测试");
        }
        System.out.println(Thread.currentThread().getName()+"--->end");
    }
}

输出:
技术图片

  • 控制台的两种输出方式
    1、注释掉上面的睡眠5秒的代码后则输出:
    技术图片
    2、控制台的输出方式总共两种,分别是:
    正常输出:System.out.println();
    发生错误时的输出:System.err.println();
    3、原因:
    可能是错误输出延迟打印到控制台了。
stop方法(不建议使用)

1、强行终止线程的执行
2、这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。
3、代码示例

public class Demo{
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();
        //5秒后强行终止t线程
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //该方法已过时(不建议使用)
        t.stop();
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+i+"--->begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"--->end");
    }
}

输出:
技术图片

合理终止线程

代码示例:

public class Demo{
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.setName("t");
        t.start();
        //睡眠5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //按意愿手动终止线程
        myRunnable.run = false;
    }
}

class MyRunnable implements Runnable{
    //做一个布尔标记
    boolean run = true;
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (run){
                System.out.println(Thread.currentThread().getName()+i+"--->begin");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                /*在终止线程之前,
                加上需要执行的操作。
                比如保存数据*/
                System.out.println("数据以保存!");
                //终止当前线程
                return;
            }
        }
    }
}

输出:
技术图片

线程调度

概述
  • 常见的线程调度模型有:
    1、抢占式调度模型:
    哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。java采用的就是抢占式调度模型。
    2、均分式调度模型:
    平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样,平均分配,一切平等。
与线程调度有关的方法
  • void setPriority(int newPriority)
    更改此线程的优先级。
    (最低优先级1;默认优先级是5;最高优先级10。)
    优先级较高的,不是优先执行;只是抢到的CPU时间片相对多一些,处于运行状态的时间多一点。
public class Demo{
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.setPriority(10);
        t.setName("t");
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}

输出:
技术图片

  • int getPriority()
    返回此线程的优先级。
public class Demo{
    public static void main(String[] args) {
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName()+"默认优先级是:"+currentThread.getPriority());
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"默认优先级是:"+thread.getPriority());
    }
}

输出:
技术图片

  • static void yield()(让位方法)
    暂停当前正在执行的线程对象,并执行其它线程。
    yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
    yield()方法的执行会让当前线程从“运行状态"回到”就绪状态"。
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            //让位给主线程
            if(i % 100 == 0){
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
  • void join()(线程合并)
    等待这个线程死亡。
    合并线程。将某线程合并到当前线程中,当前线程受阻塞,该线程执行直到结束。
public class Demo{
    public static void main(String[] args) {
        System.out.println("main begin");
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();
        /*合并线程.
        t合并到当前线程中,当前线程受阻塞,
        t线程执行直到结束。*/
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main over");
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}

输出:
技术图片

线程的同步和异步

多线程并发环境下,数据的安全问题
  • 线程安全问题
    1、为什么这个是重点?
    以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等...都已经实现完了。这些代码我们都不需要编写。
    2、最重要的是:
    要知道自己編写的程序需要放到一个多线程的环境下运行,则更需要关注的是这些数据在多线程并发的环境下是否是安全的。
    3、线程不安全的例子:
    A和B同时去银行在同一个账户(有1W)取钱,A取出1W后由于网络延迟,银行没有及时更新数据;这时B又去取了1W,银行才更新了数据,这时就出问题了。
    4、什么时候数据在多线程并发的环境下会存在安全问题呢?
    三个条件:
    条件一:多线程并发。
    条件二:有共享数据(实例变量、静态变量)。
    条件三:共享数据有修改的行为。
    满足以上3个条件之后,就会存在线程安全问题。
  • 线程同步机制
    1、怎么解决线程安全问题呢?
    线程排队执行(不能并发)。
    用排队执行解决线程安全问题。这种机制被称为:线程同步机制。专业术语叫做:线程同步。实际上就是线程不能并发了,线程必须排队执行。
    2、线程排队了就会牺牲一部分效率,但是数据安全第一位。
    3、线程同步涉及到的两个专业术语
    异步编程模型:线程1和线程2,各自执行各自的,谁也不需要等谁,其实就是多线程并发(效率较高)。
    总结:异步就是并发。
    同步编程模型:线程1和线程2,在线程1执行的时候,必须等线程2执行结束;反之亦然。两个线程之间发生了等待关系,线程排队执行(效率较低)。
    总结:同步就是排队。
模拟银行取钱的例子
public class Demo{
    private String actno;
    private double balance;

    public Demo() {
    }

    public Demo(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){
        //取款之前的余额
        double before = this.getBalance();
        //取款之后的余额
        double after = before-money;
        //模拟网络延迟(睡眠一秒)
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //更新余额
        this.setBalance(after);
    }
}
public class DemoThread extends Thread{
    //两个线程共享一个账户对象
    private Demo act;
    //构造方法传递账户对象
    public DemoThread(Demo act) {
        this.act = act;
    }

    @Override
    public void run(){
        //取款操作
        double money = 5000;
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName()+
                "账户"+act.getActno()+"取款"+money+"成功,余额为:"+
                act.getBalance());
    }
}
public class DemoTest{
    public static void main(String[] args) {
        //创建一个账户对象
        Demo demo = new Demo("act-001",10000);
        //创建两个线程
        Thread t1 = new DemoThread(demo);
        Thread t2 = new DemoThread(demo);
        //设置name
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();
        t2.start();
    }
}

输出:
技术图片

线程同步机制
  • 语法
synchronized (线程共享对象){
            //线程同步代码块
        }
  • synchronized后的括号写什么?
    1、synchronized后面小括号中传的这个“数据”,是相当关键的。这个数据必须是多线程共享的数据,才能达到多线程排队。
    2、括号中写什么?
    要看需要将哪些线程同步。假设t1、t2、t3、t4、t5,有5个线程,你只希望t1、t2、t3排队,t4、t5不需要排队。则要在()中写一个t1、t2、t3共享的对象。而这个对就对于t4、t5来说不是共享的。
  • 改进模拟银行取钱
    修改withdraw方法即可:
    public void withdraw(double money){
        /*以下这几行代码必须是线捏排队的,不能并发。
        一个线程把这里的代码全部执行结束之后,
        另一个线程才能进来。*/
        synchronized (this){
            double before = this.getBalance();
            double after = before-money;
            /*这里等待1秒钟是为了延迟结束线程,
            保证run方法中的输出语句中的
            act.getBalance()输出的是当前线程
            执行完后的余额。
            如果不睡眠的话,输出语句中的信息
            就会是两个线程结束后的信息,
            输出的余额都为0*/
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }

输出:
技术图片

synchronized

更多关于synchronized①
更多关于synchronized②
更多关于synchronized③

对synchronized的理解
  • 使用场景
    技术图片
  • 对象锁
    1、在java语言中每个对象都有一把“锁”。
    synchronized后面小括号中加的是需要上锁的对象,同对象下的A、B线程:
    技术图片
    2、在Demo中创建一个实例变量:
    Object obj = new Object();
    也可以使用:synchronized (obj){}(只要是共享对象都行)
    (在synchronized (){}大括号中的局部变量就不行)
    3、synchronized (“abc”){}也可以,“abc”在字符串常量池中是共享的。但是“abc”会使所有线程都同步,因为不是某几个共享的,而是所有线程共享的。
  • 在实例方法上使用
    synchronized出现在实例方法上,锁的是类的实例对象,这种方式不灵活。整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率低。优点就是代码少。
思考题
  • 题目一:同一对象有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
    MyClass mc = new MyClass();
class MyClass{
    public synchronized void doSome(){ }
    public void doOther(){ }
}

答:不需要,因为doOther没有synchronized关键字修饰,执行doOther不需要获取共享对象的对象锁。

  • 题目二:同一对象有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
class MyClass{
    public synchronized void doSome(){ }
    public synchronized void doOther(){ }
}

答:需要。

  • 题目三:两个对象分别有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
    MyClass mc1 = new MyClass();
    MyClass mc2 = new MyClass();
class MyClass{
    public synchronized void doSome(){ }
    public synchronized void doOther(){ }
}

答:不需要。因为有两个MyClass对象,两把锁。

  • 题目四:两个对象分别有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
class MyClass{
    public synchronized static void doSome(){ }
    public synchronized static void doOther(){ }
}

答:需要。因为这是类锁,锁的是MyClass这个类,不管创建了几个对象,这时就只有一把锁。

死锁

概述

java中的多线程

标签:安全   exception   产生   默认   方法区   src   描述   底部   demo   

原文地址:https://www.cnblogs.com/yu011/p/13058201.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!