Java多线程

1,线程的基本介绍:

1,什么是进程,什么是线程?

        进程就是一个应用程序(一个进程是一个软件)。

        线程是一个进程中的执行场景/执行单元。        

        一个进程可以开启多个线程。

2,对Java程序来说,当在DOS命令窗口中输入:

Java HelloWorld(就是执行一个Java文件)回车之后。

会先启动JVM,而JVM就是一个进程。

JVM再启动一个主线程调用main方法。

同时再启动一个垃圾回收线程负责看护,回收垃圾。

最起码,现在的Java程序中至少有两个线程并发。

一个垃圾回收线程,一个执行main方法的主线程。 

 3,进程和线程的关系:

进程可以看作是现实生活当中的公司。

线程可以看作是公司当中的某个员工。

注意:

        进程A和进程B的内存独立不共享。

在Java语言当中:

        线程A和线程B,堆内存和方法区内存共享。

        但是栈内存独立,一个线程一个栈。

        每个栈之间互不干扰,各自执行各自的,这就是多线程并发。

Java之所以有这种多线程机制,就是为了提高程序的处理效率。

4,使用了多线程机制之后,main方法结束之后,是不是,程序就结束了?

        注意:main方法结束只是主线程结束,主栈空了,其他栈(线程)可能还在压栈弹栈。

5,对于单核CPU来说, 真的可以做到真正的多线程并发吗?

1,对于多核CPU电脑来说,真正的多线程并发是没有问题的。

        每个CPU表示同一时间点上,可以真正的有多个线程并发执行。

2,什么是真正的多线程并发?

        t1线程执行t1的。

        t2线程执行t2的。

        t1不会影响t2,t2也不会影响t1。这就叫做多线程并发

3,对于单核的CPU来说,是不能做到真正的多线程并发的,

        但是可以做到给人一种多线程并发的感觉。

        对于单核CPU来说,在某一个时间点,实际只能做一件事,但是

        由于CUP的处理速度极快,多个线程之间频繁切换执行,给人的感觉就是:

        多件事情在同时做。

2,线程的生命周期:

        新建状态--就绪状态--运行状态--阻塞状态--死亡状态

3, 实现线程的第一种方式 :

        编写一个类,之间继承java.lang.Thread,重写run方法。

以下代码我们需要注意:

1,start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,

        这段代码任务完成之后(新的栈空间开辟出来了),start方法瞬间就结束了。

        线程就启动成功了,启动成功的线程会自动调用run方法,并且run方法在分支

        栈的底部(压栈)。main方法在主栈的底部。它们两是平级的。

2,如果直接调用run方法是无法开启分支线程的,开启分支线程只能运行run方法。(目前是这样)该类中的其他方法没写在run方法当中,是无法实现的。

代码示例:

public class MyThread01  extends Thread{
    @Override
    public void run(){
        //编写程序,这段程序运行在分支栈中
        for (int i=0;i<100;i++){
            System.out.println("分支线程--->"+i);
        }
    }
}
class Test{
    public static void main(String[] args) {
        //main方法,主线程
        //创建一个分支线程对象
        MyThread01 myThread01 = new MyThread01();
        //启动线程,此处调用的是Thread中的方法
        myThread01.start();
        
        //如果直接调用
        //这只是普通的方法调用,还是在主线程中,并不会开启分支线程
        //myThread01.run();
        
        //主线程运行的代码 
        for (int i=0;i<100;i++){
            System.out.println("主线程--->"+i);
        }
    }
}

多线程内存示意图: 

4, 实现线程的第二种方式 :

        编写一个类,实现java.lang.Runnable接口,实现run方法。

1,java.lang.Runnable接口,只有以下一个方法:

           public abstract void run();

        要开始分支线程还是得借助Thread类的另外一个构造方法

        参数是Runnable 类型(把实现Runnable接口的类传进去),

        Thread类才有start方法开启分支线程。

2,java.lang.Thread类实现了这个接口,可以使用匿名内部类的方式直接开启分支线程。

代码示例:

1,不使用匿名内部类:

public class MyThread02 implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("分支线程:"+i);
        }
    }
}
class Test01{
    public static void main(String[] args) {
        //创建线程对象
        Thread thread = new Thread(new MyThread02());
        //开启分支线程
        thread.start();
        for (int i=0;i<100;i++){
            System.out.println("主线程:"+i);
        }
    }
}

2,使用匿名内部类:

使用匿名内部类,这里 可以 直接new接口

public class MyThread03 {
    //主线程
    public static void main(String[] args) {
        //使用匿名内部类方式,这个类没有名字可以 直接new接口
        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);
        }
    }
}

5, 实现线程的第三种方式 :

编写一个类实现Callable接口。(JDK8新特性),重写call方法。

这种方式实现的线程可以获取线程的返回值。

上面两种方法都是无法获取线程返回值的,

        

如果系统委派一个线程去执行一个任务,该线程执行完成后,可能会有一个执行结果,

这时候就可以使用这个接口。

        

部分源码:

可以看出FutureTask是Runnable的子类:可以使用Thread类的构造方法创建分支线程

        public class FutureTask<V> implements RunnableFuture<V>{}

        public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }

        

        public FutureTask(Callable<V> callable){}其中一个构造方法。

1,没有使用匿名内部类代码示例: 

public class MyThread04 implements Callable{

    @Override
    public Object call() throws Exception {
        //call方法相当于run方法,只不过这个有返回值
        System.out.println("call begin");
        System.out.println(new Date());
        Thread.sleep(1000);//线程休眠,后面会讲
        System.out.println(new Date());
        System.out.println("call end");
        int a=100;
        int b=100;
        return a+b;//自动装箱
    }
}
class Test03{
    public static void main(String[] args) {
        //第一步创建一个“未来任务类”对象
        FutureTask futureTask = new FutureTask(new MyThread04());
        //创建线程对象
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();

         //怎么把分支t线程的返回结果
        //该方法需要处理异常
        //如果此方法后面还有代码必须得等到拿到结果,就是线程执行完
        Object o = futureTask.get();
    }
}

 1,使用匿名内部类代码示例: 

public class MyThread05 {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new Callable(){
            @Override
            public Object call() throws Exception {
                //call方法相当于run方法,只不过这个有返回值
                int a=100;
                int b=100;
                return a+b;//自动装箱
            }
        });
        //创建线程对象
        Thread thread = new Thread(futureTask);
        //开启线程
        thread.start();
    }
}

6,获取/修改当前线程的名字:

使用以下Thread类中的三个方法:

        .setName()//修改名字

        .getName()//获取当前线程名字

.currentThread()//在哪个线程方法中,就获取哪个线程对象

默认名字:Thread-0

                  Thread-1

                   .....

代码示例: 

   1,     .setName()//修改名字

              .getName()//获取当前线程名字

public class MyThread06 {
    public static void main(String[] args) {
        //创建线程对象
        MyThread01 t1= new MyThread01();
        //没有设置线程名字
        System.out.println(t1.getName());//Thread-0
        //手动设置线程名字
        t1.setName("我是当前线程");
        System.out.println(t1.getName());//我是当前线程

        MyThread01 t2 = new MyThread01();
        System.out.println(t2.getName());//Thread-1
    }
}

2,.currentThread()

        在哪个线程方法中,就获取哪个线程对象:

        可以用来获取主线程对象。

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

        Test04 t1= new Test04();
        t1.setName("t1");
        System.out.println(t1.getName());
        Test04 t2 = new Test04();
        t2.setName("t2");
        System.out.println(t2.getName());
        t1.start();
        t2.start();

        //获取当前线程对象---也就是main线程的对象
        //this.getName()这种方式是不行的
        Thread thread = Thread.currentThread();
        System.out.println("主线程的名字:"+thread.getName());
    }
}
class Test04 extends Thread{

    @Override
    public void run() {
        //哪个线程调用run方法就是创建的哪个线程对象
        Thread thread = Thread.currentThread();
        System.out.println("分支线程的名字:"+thread.getName());
    }
}
结果:
t1
t2
主线程的名字:main
分支线程的名字:t1
分支线程的名字:t2

7,休眠方法:

使用sleep方法:

        1,源码

        public static native void sleep(long millis) throws InterruptedException;

可以看出是一个静态方法,直接使用类名就可以调用,参数是毫秒,底层是用C++实现

作用:

让当前进程进入休眠,进入阻塞状态,放弃占有的CPU时间片,让给其他线程使用

可以做到每隔几秒运行一段代码。

需要注意的是:

此方法是让当前进程进入阻塞状态,即使使用非本线程的对象使用此方法,依旧是让本线程进入阻塞状态。

代码示例:

public class MyThread08 {
    public static void main(String[] args) {
        
        try {
            //获取执行到此的时间
            System.out.println(new Date());
            //隔五秒运行下面的代码,需要处理异常
            Thread.sleep(5000);
            //获取执行到此的时间
            System.out.println(new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果:
Thu Sep 22 14:25:34 CST 2022
Thu Sep 22 14:25:39 CST 2022

注意以下代码可以发现:

1,sleep方法是让当前进程进入阻塞状态,即使使用非本线程的对象使用此方法,依旧是让本线程进入阻塞状态。

2,还要注意一点,在run方法中使用sleep方法只能使用try--catch方式处理异常,不能抛出

因为这个方法是继承重写的方法,在父类这个方法是没有异常处理的

public class MyThread09 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Test05());
        t1.start();
        try {

            System.out.println("这里是主线程前:"+new Date());
            t1.sleep(5000);//注意这里不是把t1分支线程休眠,而是主线程
            System.out.println("这里是主线程后:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
class Test05 implements Runnable{

    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        try {
            System.out.println("这里是分支线程前:"+new Date());
            thread.sleep(5000);
            System.out.println("这里是分支线程后:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果:
这里是分支线程前:Thu Sep 22 14:38:53 CST 2022
这里是主线程前:Thu Sep 22 14:38:53 CST 2022
这里是分支线程后:Thu Sep 22 14:38:58 CST 2022
这里是主线程后:Thu Sep 22 14:38:58 CST 2022

1,中断休眠:

   .interrupt();

                终止t线程的睡眠(这种中断睡眠的方式已考虑java的异常机制)

还有一个.stop方法;

        通过线程对象调用,把线程强行五秒后终止。(不推荐使用,已过时)

public class MyThread01 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Test01());
        t1.start();
        //终止t线程的睡眠(这种中断睡眠的方式已考虑java的异常机制)
        t1.interrupt();//干扰,
        try {

            System.out.println("这里是主线程前:"+new Date());
            t1.sleep(5000);//注意这里不是把t1分支线程休眠,而是主线程
            System.out.println("这里是主线程后:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Test01 implements Runnable{

    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        try {
            System.out.println("这里是分支线程前:"+new Date());
            thread.sleep(5000);
            System.out.println("这里是分支线程后:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 线程调度:

线程调度就是操作系统为线程分配处理器使用权的过程。

1,常见的线程调度模型:

        抢占式调度模型:

                哪个线程的优先级比较高,强到的CPU时间片的概率就高一些/多一些

                这里的多一些是指”运行状态“的时间更长

                Java采用的就是抢占式调度模型

        均分式调度模型:

                平均分配CPU时间片。每个线程占有的时间片的时间长度一样,概率一样

 优先级的设置:

public final void setPriority(int newPriority)

                设置线程的优先级

public final int getPriority() { return priority; }

                获取线程的优先级

优先级最低是1,最高时10,默认时5

优先级比较高的获取CPU时间片可能会多一些。(但不是一定,大概率是高的)。

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

        Thread t1= new Thread(new Test());
        t1.setName("t1");
        //设置优先级
        t1.setPriority(10);
        //获取主线程对象
        Thread t2= Thread.currentThread();
        //获取主线程的优先级
        System.out.println(t2.getPriority());
        //开启线程
        t1.start();
        for (int i=0;i<100;i++){
            System.out.println(t2.getName()+":"+i);
        }
    }
}
class Test  implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<100;i++){
            //获取使用run方法线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

让位方法:

源码:

public static native void yield();

        

是一个静态方法,作用是让当前正在执行的线程对象暂停,并执行其它线程。

        

注意 :该方法不是阻塞方法,它是让当前线程让位,让给其他线程使用,

使当前线程从“运行状态”进入“就绪状态”

但是在回到就绪状态之后,又可能还会再次强到CPU时间片,进入运行状态。

 

多线程并发/线程同步:

1,关于多线程并发环境下,数据的安全问题:

        以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,对象的创建,线程的启动等,都已经实现完了。这些代码我们不需要编写,所以我么更需要注意的是,把自己编写的程序放到多线程的环境下运行,数据安全在多线程并发环境下是否是安全的。

2,什么时候数据在多线程并发的环境下会出现安全问题?

        三个条件:

                1,多线程并发

                2,有共享数据

                3,共享数据有修改行为

3,怎么解决线程安全问题?

1,线程排队执行(不能并发)。这种机制被称为“线程同步机制”

2,线程同步就是线程排队了,线程排队是会牺牲一部分效率的,没办法,数据安全是第一位的,只有做到数据安全,才考虑效率问题。

两个专业术语:

异步编程模型:

        线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,

        谁都不需要等谁,这种编程模型叫做:异步编程模型。

        其实就是多线程并发(效率较高)

同步编程模型:

          线程t1和线程t2,各自执行各自的,在t1执行的时候必须等t2执行结束,或者相反。

        两个线程之间发生了等待关系,这就是同步编程模型。

        效率较低,但是数据安全。就是线程排队执行

并发和并行的区别:

并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。 并行(parallelism)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行

以一个简单的取钱例子说明并发带来的问题:

有一个只有10000块的账户,取了两次5000,还剩5000

建立一个账户类:

public class Account {
    //账号
    private String actor;
    //余额
    private double balance;

    //取款的方法
    public void withdraw(double money){
        //t1和t2并发这个方法,(t1和t2是两个栈。两个栈操作同一个对象)
        //取款之前的余额
        double before=this.getBalance();
        //取款之后的余额
        double after=before-money;
        //更新余额
        //如果t1线程执行到了这里,但是还没来的及更新,t2线程就进来withdraw(取款了)
        //这个时候,就会出现取钱出现问题
        this.setBalance(after);
        System.out.println("还剩下余额:"+after);
    }

    public Account() {
    }

    public Account(String actor, double balance) {
        this.actor = actor;
        this.balance = balance;
    }

    public String getActor() {
        return actor;
    }

    public void setActor(String actor) {
        this.actor = actor;
    }

    public double getBalance() {
        return balance;
    }

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

    @Override
    public String toString() {
        return "Account{" +
                "actor='" + actor + '\'' +
                ", balance=" + balance +
                '}';
    }
}

线程类(就是可同时发生的行为):

public class AccountThread extends Thread{

    //两个线程必须共享一个账户对象
    private Account act;

    //通过构造方法传递过来账户对象
    public AccountThread(Account act){
        this.act=act;
    }

    @Override
    public void run() {
        //run方法的执行表示取款操作。
        //假设取款5000
        double money=5000;
        //取款
        //多线程并发执行这个方法
        act.withdraw(money);
    }
}

测试结果:

注意,并不是一定会出现下面的情况,只是可能会出现

public class MyThread04 {
    public static void main(String[] args) {
        //创建一个对象
        Account act = new Account("act-001", 10000);
        //创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);

        t1.setName("t1");
        t2.setName("t2");
        //启动线程
        t1.start();
        t2.start();
    }
}
结果:
还剩下余额:5000.0
还剩下余额:5000.0

解决办法:

线程同步机制:

使用线程同步机制的语法:

synchronized(){

        //线程同步代码块

}

        

使用这个方法之后,必须得等到第一个进入的线程 完成之后,第二个线程才能进入(得来了解Java中锁得概念,看后面)

synchronized后面的小括号中传的这个-数据-是相当关键的,这个数据必须是多线程共享的数据。才能达到多线程排队。

()中写什么?

        这要看你想要哪些线程同步。

        假设t1,t2,t3,t4,t5,有五个线程

        你只希望t1 t2, t3排队, t4, t5 不需要排队,怎么办?

        你一定要在()中写一个t1 t2, t3共享的对象,而这个对象对于t4, t5不共享

我们上面的例子共享对象是:账户对象,账户对象是共享的,那么this就是账户对象,

不一定是this,这里只要是多线程共享的哪个对象就行。

如下面部分代码: 

1,同步代码块,写在代码块外面,里面的共享对象可以自己定

    //取款的方法
    public void withdraw(double money) {

       // synchronized (actor)//可以使用实例变量,
        // 当一个对象是共享时,它的实例变量也是共享的
          synchronized (this){
            //t1和t2并发这个方法,(t1和t2是两个栈。两个栈操作同一个对象)
            //取款之前的余额
            double before = this.getBalance();
            //取款之后的余额
            double after = before - money;
            //更新余额
            //如果t1线程执行到了这里,但是还没来的及更新,t2线程就进来withdraw(取款了)
            //这个时候,就会出现取钱出现问题
            this.setBalance(after);
            System.out.println("还剩下余额:" + after);
        }
    }

        1.2 这种方式会扩大同步的范围,效率更低了

   @Override
    public void run() {
        //run方法的执行表示取款操作。
        //假设取款5000
        double money=5000;
        //取款
        //多线程并发执行这个方法
        synchronized (act) {
            act.withdraw(money);
        }
    }

 2,在实例方法上使用synchronized

        注意:这种方式表示共享对象一定时this,并且同步代码块是整个方法体。

  public synchronized  void withdraw(double money) {

       // synchronized (actor)//可以使用实例变量,
        // 当一个对象是共享时,它的实例变量也是共享的
        //synchronized (this){
            //t1和t2并发这个方法,(t1和t2是两个栈。两个栈操作同一个对象)
            //取款之前的余额
            double before = this.getBalance();
            //取款之后的余额
            double after = before - money;
            //更新余额
            //如果t1线程执行到了这里,但是还没来的及更新,t2线程就进来withdraw(取款了)
            //这个时候,就会出现取钱出现问题
            this.setBalance(after);
            System.out.println("还剩下余额:" + after);
        }

 

Java“锁”的概念:

在Java语言中,任何一个对象都有一把“锁”,其实这把锁就是标记(只是把它叫做锁)

100个对象,100把锁,一个对象一把锁。

        

以下代码的执行原理?

        1,假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。

        2,假设t1先执行了,遇到synchronized,这个时候自动找“后面共享对象”的对象锁,

        找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中,一直

        都是占有这把锁的。直到同步代码块代码执行结束,这把锁才会释放

        3,假设t1已经占有这把锁了,此时t2也遇到synchronized,也会去自动找

        “后面共享对象”的对象锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束

        直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁了,

        然后t2占有这把锁,进入同步代码块的执行。

        

        这样就达到了线程排队执行。

        这里需要注意的是:这个共享对象一定的选好了。这个共享对象一定是你需要排队执行

        的这些线程对象所共享的。

注意:如果一个对象时共享的,那么对象里面的实例变量也是共享的。

锁池的概念: 

遇到synchronized会在锁池里面找共享对象的锁。

线程进入锁池找共享对象的时候,会释放之前占有的CPU时间片,有可能找到了,有肯没有找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢占CPU时间片

 总结:

synchronized的三种写法:

第一种:同步代码块

        灵活

synchronized(){

        //线程同步代码块

}

第二种:在实例方法上使用synchronized

        表示共享对象一定是this        

       并且同步代码块是整个方法体

第三种:在静态方法上使用synchronized

        表示找类锁

        类锁永远只有一把。

就算创建了100个对象,那类锁也只有一把

(区别看下面的截图)

对象锁:一个对象一把锁,100个对象100把锁

类锁:100个对象,也可能是一把类锁。

对象锁:

类锁: 

 

 Java变量的线程安全问题:

实例变量:在堆中。

静态变量:在方法去中。

局部变量:在栈中。

        

局部变量永远不会存在线程安全问题

因为局部变量不共享。

实例变量和静态变量是有可能会出现线程安全问题的,它们多线程是共享的

守护线程:

Java语言中线程分为两大类:

        1,用户线程

        2,守护线程(后台线程)

        其中具有代表性的就是:垃圾回收线程(守护线程)

守护线程的特点:

        一般守护线程是一个死循环,所有的用户线程只要结束,

        守护线程自动结束

注意:主线程main方法是一个用户线程。

        

用户线程一般用在什么地方?

假设每天00:00的时候系统数据自动备份。

这个时候需要使用到定时器,并且我可以将定时器设置为守护线程。

一直在那里看着,每到00:00的时候就备份一次。所有的用户线程

如果结束了,守护线程自动退出,没有必要进行数据备份了。

        

守护线程的创建 :

只需要在启动线程之前,使用方法“对象.setDaemon(ture)”,就可以把线程设置为守护线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值