如何创建多线程以及解决线程安全问题

多线程

并发:指两个或多个事件在同一时间段内发生(交替执行)

并行:指两个或多个事件在同一时刻发生(同时发生)(一起同行)

image-20210707100652738

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程

**进程:**是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

image-20210707101256497

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

image-20210707102329236

线程调度

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

创建线程类

主线程:执行主(main)方法的线程

单线程程序:java程序中只有一个线程,执行从main方法开始,从上到下依次执行

image-20210707104839704

创建多线程程序的第一种方式:创建Thread类的子类

java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类

实现步骤:

​ 1、创建一个Thread类的子类

​ 2、在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)

​ 3、创建Thread类的子类对象

​ 4、调用Thread类中的方法start方法,开启新的线程,执行run方法

​ void start() 使此线程开始执行; Java虚拟机调用此线程的run方法。

​ 结果是两个线程并发的执行(交替执行);当前线程 (main线程)和另一个线程(创建的新线程,执行其run方法)。

​ 注意: 1、多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动。

​ 2、java程序属于抢占式调度,哪个线程的优先级高,哪个线程优先执行;同一优先级,随机选择一个执行

package com.itheima.Thread;

//1.创建一个Thread类的子类
public class MyThread extends Thread {
    //	2、在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            System.out.println("mythread:"+i);
        }

    }
}

package com.itheima.Thread;

public class MyThreadDemo01 {
    public static void main(String[] args) {
        //3.创建Thread类的子类对象
        MyThread mt=new MyThread();
        //4、调用Thread类中的方法start方法,开启新的线程,执行run方法
        mt.start();
        for (int i = 0; i <20 ; i++) {
            System.out.println("main:"+i);
        }

    }
}

多线程原理

多线程随机打印结果原理

JVM执行main方法,找os开辟一条main方法通向cpu的路径,这个路径叫main线程,主线程cpu通过这个线程,这个路径可以执行main方法。又因为创建了Thread类的子类对象,所以又开辟了一条通向cpu的新路径,用来执行run方法

image-20210707114639718

多线程内存图解

image-20210707115557700

即直接使用run方法和调用start方法还是有区别的。

直接调用run方法:那么run方法会进行压栈操作,和main方法在同一个栈内,这时就变成了单线程。

调用start方法:它会再开辟新的栈空间,然后执行run方法,实现了多线程。

创建多线程程序的第二种方式:实现Runnable接口

java.lang.Runnable ,Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。

java.lang.Thread类的构造方法

Thread(Runnable target) 分配一个新的 Thread对象。
Thread(Runnable target, String name) 分配一个新的 Thread对象。

实现步骤

1、创建一个Runnable接口的实现类

2、在实现类中重写Runnable接口的run方法,设置线程任务

3、创建一个Runnable接口的实现类对象

4、创建Thread类对象,构造方法中传递Runnable接口的实现类对象

5、调用Thread类中的start方法,开启新的线程执行run方法

package com.itheima.Thread;
/*1、创建一个Runnable接口的实现类

2、在实现类中重写Runnable接口的run方法,设置线程任务*/
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }

    }
}

package com.itheima.Thread;

public class MyRunnableDemo01 {
    public static void main(String[] args) {
        //*3、创建一个Runnable接口的实现类对象
        Runnable mr=new MyRunnable();
        //4、创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread th=new Thread(mr);
       // 5、调用Thread类中的start方法,开启新的线程执行run方法
        th.start();
        for (int i = 0; i <20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }

    }

}

Thread类

构造方法:

  • public Thread() :分配一个新的线程对象。

  • public Thread(String name) :分配一个指定名字的新的线程对象。

  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。

  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法:

  • public String getName() :获取当前线程名称。

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public void run() :此线程要执行的任务在此处定义代码。

  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

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

package com.itheima_01;

public class MyThread extends Thread {
    @Override
    public void run() {
        //链式编程
        System.out.println(Thread.currentThread().getName());
    }
}

package com.itheima_01;

public class MyThreadDemo01 {
    public static void main(String[] args) {
        //创建Thread子类的对象
        MyThread mt=new MyThread();
        //调用start方法
        mt.start(); //Thread-0
        new MyThread().start(); //Thread-1
        //currentThread(),静态方法,所以可以直接类名.名称调用,返回对当前正在执行的线程对象的引用

        System.out.println(Thread.currentThread().getName()); //main

    }
}

package com.itheima_01;
/*
* public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。*/
public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        //模拟秒表
        for (int i = 1; i <=60 ; i++) {
            System.out.println(i);
            //使用Tread类的sleep方法让程序睡眠一秒钟(1000毫秒)
            Thread.sleep(1000);
        }
    }
}

Thread和Runnable的区别

实现Runnable接口创建多线程程序的好处:

1、避免了单继承的局限性

​ 一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类

​ 实现了Runnable接口,还可以继承其他的类,实现其他的接口

2、增强了程序的扩展性,降低了程序的耦合性

​ 实现了Runnable接口的方式,把设置线程任务和开启新线程进行了分离

​ 分离:实现类中,重写了run方法:用来设置线程任务

​ 创建Thread类对象,调用start方法:用来开启新线程

匿名内部类方式实现线程的创建

匿名:没有名字。 内部类:写在其他类的内部

匿名内部类作用:简化代码

​ 把子类继承父类,重写父类的方法,创建子类对象合一步完成

​ 把实现类实现类接口,重写接口中的方法,创建实现类对象合一步完成

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

package com.itheima_01;

public class InnerClassThreadDemo {
    public static void main(String[] args) {
        //用Thread方法和匿名内部类创建线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        }.start();
        //用Runnable和匿名内部类实现创建线程
        //Runnable r=new RunnableImpl() 多态
        Runnable r=new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName()+"小暑");
                }
            }
        };
        new Thread(r).start();
        //上面代码的简化
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName()+"夏天");
                }
            }
        }).start();

    }
}

线程安全

概述:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样

的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个

(本场电影只能卖100张票)。 (多个窗口一起卖这100张票)

image-20210707150509903

用代码演示三个窗口买票引起的线程安全问题

package com.itheima_02;

public class RunnableImpl implements Runnable {
    private  int ticket=100;
    @Override
    public void run() {
        while(true){
            if(ticket>0){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}

package com.itheima_02;

public class Demo01Ticket {
    public static void main(String[] args) {
        //创建Runnab的实现类对象
        RunnableImpl r=new RunnableImpl();
        //创建Thread的对象,并采用带参的构造方法
        //创建三个线程
        Thread t0=new Thread(r);
        Thread t1=new Thread(r);
        Thread t2=new Thread(r);
        t0.start();
        t1.start();
        t2.start();
    }
}

发现程序出现了两个问题:

  1. 相同的票数,比如5这张票被卖了两回。

  2. 不存在的票,比如0票与-1票,是不存在的。

线程安全问题产生的原理

image-20210707152701916

解决线程安全问题:同步代码块

synchronized(['sɪŋkrənaɪzd])

格式:

synchronized(锁对象){

​ 可能会出现线程安全问题的代码(访问了共享数据的代码)

}

注意:1、通过代码块中的锁对象,可以使用任意的对象

​ 2、但是必须保证多个线程使用的锁对象是同一个

​ 3、锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行

package com.itheima.Safe;

/*
    卖票案例出现了线程安全问题
    卖出了不存在的票和重复的票

    解决线程安全问题的一种方案:使用同步代码块
    格式:
        synchronized(锁对象){
            可能会出现线程安全问题的代码(访问了共享数据的代码)
        }

    注意:
        1.通过代码块中的锁对象, 
        2.但是必须保证多个线程使用的锁对象是同一个
        3.锁对象作用:
            把同步代码块锁住,只让一个线程在同步代码块中执行
 */
public class RunnableImpl1 implements Runnable{
    //定义一个多个线程共享的票源
    private  int ticket = 100;

    //创建一个锁对象
    Object obj = new Object();

    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while(true){
            //同步代码块
            synchronized (obj){
                //先判断票是否存在
                if(ticket>0){
                    //提高安全问题出现的概率,让程序睡眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //票存在,卖票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                }
            }
        }
    }
}

同步技术的原理

image-20210707161215140

同步技术的原理:

使用一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器

3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票

t0抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这是t0会检查synchronized代码块是否有对象,发现有,就会获取锁对象,进入到同步中执行

t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这是t1会检查synchronized代码块是否有锁对象,发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象。一直到t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步

同步方法

是解决线程安全问题的第二种方法。

格式:

public synchronized void method(){

可能会产生线程安全问题的代码

}

同步锁是谁? 

对于非static方法,同步锁就是this。 

对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。 

使用步骤:1、把访问了共享数据的代码抽取出来,放到一个方法中

2、在方法上添加synchronized修饰符

同步方法也会把方法内部的代码锁住,只让一个线程执行。那么同步方法的锁对象是谁?就是实现类对象 new RunnableImpl() ,也就是this

package com.itheima.Synchronized;

public class RunnableImpl1 implements Runnable{
    //定义一个多个线程共享的票源
    private  int ticket = 100;


    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicket();

        }
    }

    //定义一个同步方法
    public synchronized void payTicket(){
        //同步代码块
        /*synchronized (this){*/
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        /*}*/

    }
}


静态同步方法

静态同步方法的锁对象不能是this。因为this是创建对象之后产生的,静态方法优先于对象。静态方法的锁对象是本类的class属性-------》class文件对象(反射)

package com.itheima.StaticSynchronized;

public class RunnableImpl1 implements Runnable{
    //定义一个多个线程共享的票源
    private static int ticket = 100;


    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicket();

        }
    }

    //定义一个同步方法
    public static synchronized void payTicket(){
        //同步代码块
        synchronized (RunnableImpl1.class){
            //先判断票是否存在
            if(ticket>0){//变量要定义成静态的,因为静态的访问静态的
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }

    }
}

Lock锁

是解决线程安全问题的第三种方法。

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,

同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock() :加同步锁。

  • public void unlock() :释放同步锁。

使用步骤:1、在成员位置创建一个ReentrantLock对象

​ 2、在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁

​ 3、在可能会出现安全问题的代码后调用Lock接口中的方法unLock释放锁(通常将其放在finally里,即不管代码是否有异常,都会执行释放锁)

package com.itheima.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class RunnableImpl1 implements Runnable{
    //定义一个多个线程共享的票源
    private  int ticket = 100;
    //1、在成员位置创建一个ReentrantLock对象
    ReentrantLock L=new ReentrantLock();

    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while(true){
            //2、在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
            L.lock();
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                    //票存在,卖票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    L.unlock();
                }


            }

        }
    }


}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值