Java-多线程、同步代码块解决线程安全问题

Java-多线程、同步代码块解决线程安全问题_第1张图片

进程概述及多进程的意义

  • 线程和进程
    要想说线程,首先必须得聊聊进程,因为线程是依赖于进程存在的。
  • 进程概述
    什么是进程呢?通过任务管理器我们就可以看到进程的存在。
    概念:进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。
    每一个进程都有它自己的内存空间和系统资源。
  • 多进程的意义
    单进程计算机只能做一件事情。而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),
    所以我们常见的操作系统都是多进程操作系统。比如:Windows,Mac和Linux等,能在同一个时间段内执行多个任务。
    对于单核计算机来讲,游戏进程和音乐进程是同时运行的吗?不是。
    因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,
    所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。多进程的作用不是提高执行速度,而是提高CPU的使用率。

线程概述及多线程的意义及并行和并发的区别

  • 什么是线程
    在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。是程序使用CPU的基本单位。所以,进程是拥有资源的基本单位, 线程是CPU调度的基本单位。

  • 多线程有什么意义呢?
    多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。
    那么怎么理解这个问题呢?
    我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到
    CPU的执行权的概率应该比较单线程程序抢到的概率要大.那么也就是说,CPU在多线程程序
    中执行的时间要比单线程多,所以就提高了程序的使用率.但是即使是多线程程序,那么他们
    中的哪个线程能抢占到CPU的资源呢,这个是不确定的,所以多线程具有随机性.

  • 两个词汇的区别:并行和并发。
    前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
    后者是物理上同时发生,指在某一个时间点同时运行多个程序。

    • 并发 : 指应用能够交替执行不同的任务, 其实并发有点类似于多线程的原理, 多线程并非是如果你开两个线程同时执行多个任务。
      执行, 就是在你几乎不可能察觉到的速度不断去切换这两个任务, 已达到"同时执行效果", 其实并不是的, 只是计算机的速度太快, 我们无法察觉到而已. 就类似于你, 吃一口饭喝一口水, 以正常速度来看, 完全能够看的出来, 当你把这个过程以n倍速度执行时…可以想象一下.
    • 并行 : 指应用能够同时执行不同的任务, 例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行

Java程序运行原理和JVM的启动是多线程的吗

  • Java程序运行原理
    Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
    该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。
    所以 main方法运行在主线程中。
  • JVM的启动是多线程的吗: JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

三种多线程程序实现

  • 如何实现多线程:

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
但是Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。

  • 几个小问题:
    启动线程使用的是哪个方法-------start()方法
    线程能不能多次启动-------执行多次会报错

  • run()和start()方法的区别
    我们启动线程使用不是run方法,而应该是start方法.使该线程开始执行;
    Java 虚拟机调用该线程的 run 方法。

  • 为什么要重写run方法?
    这个类是一个线程类,那么在这个类中我们可不可以写一些其他的方法呢?
    我们可以在写其他的方法,那么其他方法中封装的代码都是需要被我们线程执行的吗? 不一定
    那么也就是run方法中封装应该是必须被线程执行的代码.

  • run方法中的代码的书写原则: 一般是比较耗时的代码

  • 多线程程序实现的方式1
    (1):创建一个继承Thread的类并重写run()方法
    (2):创建该类对象
    (3):用对象开启线程

  • 案例演示: 多线程程序实现的方式1

//第一步操作:创建一个继承Thread的类并重写run()方法
public class MyThread extends Thread{
    @Override
    public void run() {
        //这个run方法就是需要线程来执行的代码,一般耗时的操作,我们就会写在run方法里面,让线程去执行
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
------------------------------------------------------------------------------
public class MyTest3 {
    public static void main(String[] args) {
        //第二步操作:在测试类中创建第一步创建的类的对象
        MyThread th = new MyThread();
        //用创建好的对象调用start()方法开启线程
        th.start();
    }
}
  • 多线程程序实现的方式2
    (1):创建一个实现Runnable接口的类并重写run()方法
    (2):创建该类对象
    (3):分配新的Thread对象
    (4):用对象开启线程
  • 实现接口方式的好处:
    可以避免由于Java单继承带来的局限性。
  • 案例演示:多线程程序实现的方式2
//第一步操作:创建一个实现Runnable接口的类并重写run()方法
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //需要线程执行
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
------------------------------------------------------------------------------
public class MyTest {
    public static void main(String[] args) {
    	//第二步操作:在测试类中创建第一步创建的类的对象
        MyRunnable myRunnable = new MyRunnable();
        //Thread(Runnable target)
        //第三步操作:分配新的 Thread 对象。
        Thread th = new Thread(myRunnable);
        //第四步操作:用对象开启线程
        th.start();
        new MyThread().start();
    }
}
  • 多线程程序实现的方式3
    (1):创建一个实现Callable接口的类(有泛型接口)并重写call()方法
    (2):创建该类对象
    (3):创建一个FutureTask类将Callable接口的子类对象作为参数传进去
    (4):分配新的Thread对象
    (5):用对象开启线程

  • 实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。

  • 执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类

  • 案例演示:多线程程序实现的方式3

//第一步操作:创建一个实现Callable接口的类(有泛型接口)并重写call()方法
public class MyCallable implements Callable {
    //call方法就是线程要执行的方法
    @Override
    public Integer call() throws Exception {
        System.out.println("线程执行了");
        int sum=0;
        for (int i = 1; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}
-------------------------------------------------------------------------------
public class MyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
		//第二步操作:在测试类中创建第一步创建的类的对象
        MyCallable myCallable = new MyCallable();
        //第三步操作:创建一个FutureTask类将Callable接口的子类对象作为参数传进去
        FutureTask task = new FutureTask<>(myCallable);
        //第四步操作:分配新的Thread对象
        Thread thread = new Thread(task);
        //第五步操作:用对象开启线程
        thread.start();
        //线程执行完之后,可以获取结果
        Integer integer = task.get();
        System.out.println(integer);
    }
}

获取和设置线程对象名称

  • Thread类的基本获取和设置方法
    public final String getName()//获取线程名称
    public final void setName(String name)//设置线程名称
    其实通过构造方法也可以给线程起名字
  • Thread类的通用获取
    Thread.currentThread.getName()
  • 案例演示: 获取和设置线程对象名称
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
-------------------------------------------------------------------------------
public class MyTest {
    public static void main(String[] args) {
        //我如何获取主线程名称
        Thread thread = Thread.currentThread();//获取当前线程对象
        thread.setName("这是主线程");
        String name = thread.getName();
        System.out.println(name);
        System.out.println("main方法执行了");
        MyThread myThread = new MyThread();
        MyThread myThread2 = new MyThread();
        //设置线程的名称
        myThread.setName("刘亦菲");
        myThread2.setName("范冰冰");
        myThread.start();
        myThread2.start();

    }
}

线程调度及获取和设置线程优先级

  • 线程的执行
    假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,
    线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
  • 线程有两种调度模型:
    • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
    • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,
    • 优先级高的线程获取的 CPU 时间片相对多一些。
      Java使用的是抢占式调度模型。
  • 如何设置和获取线程优先级
    public final int getPriority() //获取线程的优先级
    public final void setPriority(int newPriority)//设置线程的优先级
  • 注意事项: 有的时候我们给线程设置了指定的优先级,但是该线程并不是按照优先级高的线程执行,那是为什么呢?
    • 因为线程的优先级的大小仅仅表示这个线程被CPU执行的概率增大了.但是我们都知道多线程具有随机性,
    • 所以有的时候一两次的运行说明不了问题
  • 案例演示: 获取和设置线程优先级
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
-------------------------------------------------------------------------------
public class MyTest {
    public static void main(String[] args) {
        //Java 使用线程的调度模型,是抢占式调度
        //如果说多个线程的优先级一样,线程的执行,就是随机抢占
        //我们如何设置线程的优先级
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        //设置线程的优先级 1----10
        th1.setPriority(10);
        th2.setPriority(Thread.MAX_PRIORITY);
        //获取线程的优先级
        int priority = th1.getPriority(); //默认优先级是5
        int priority1 = th2.getPriority();
        System.out.println(priority);
        System.out.println(priority1);
        th1.setName("李冰冰");
        th2.setName("范冰冰");
        th1.start();
        th2.start();
    }
}

线程控制

  • 线程休眠:public static void sleep(long millis) :线程休眠

  • 加入线程:public final void join():等待该线程执行完毕了以后,其他线程才能再次执行

  • 礼让线程:public static void yield(): 暂停当前正在执行的线程对象,并执行其他线程。 (效果不明显,暂停的时候其他线程可能抢占不到CPU的执行权)

  • 守护线程:public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。(该方法必须在启动线程前调用)

  • 中断线程:
    public final void stop():停止线程的运行
    public void interrupt():中断线程(这个翻译不太好),查看API可得当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞

  • Java用户线程和守护线程

    • 1.用户线程和守护线程的区别
      用户线程和守护线程都是线程,区别是Java虚拟机在所有用户线程dead后,程序就会结束。而不管是否还有守护线程还在运行,若守护线程还在运行,则会马上结束。很好理解,守护线程是用来辅助用户线程的,如公司的保安和员工,各司其职,当员工都离开后,保安自然下班了。

    • 2.用户线程和守护线程的适用场景
      由两者的区别及dead时间点可知,守护线程不适合用于输入输出或计算等操作,因为用户线程执行完毕,程序就dead了,适用于辅助用户线程的场景,如JVM的垃圾回收,内存管理都是守护线程,还有就是在做数据库应用的时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监听连接个数、超时时间、状态等。

    • 3.创建守护线程
      调用线程对象的方法setDaemon(true),设置线程为守护线程。
      1)thread.setDaemon(true)必须在thread.start()之前设置。
      2)在Daemon线程中产生的新线程也是Daemon的。
      3)不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。
      因为Daemon Thread还没来得及进行操作,虚拟机可能已经退出了。

    • 4.Java守护线程和Linux守护进程
      两者不是一个概念。Linux守护进程是后台服务进程,没有控制台。
      在Windows中,你可以运行javaw来达到释放控制台的目的,在Unix下你加&在命令的最后就行了。所以守护进程并非一定需要的。

继承Thread类的方式卖电影票案例

  • 案例演示
    需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
    通过继承Thread类实现

  • 分析:
    a: 三个窗口其实就是3个线程
    b: 定义票的数量100张
    c: 创建线程对象,启动线程. 每卖一张这个票数应该-1

public class MyThread extends Thread{
    static int tickets = 100;
    @Override
    public void run() {

        while (tickets>0){
            if(tickets>=1){
                String name = this.getName();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(tickets>=1) {
                    System.out.println(name + "正在出售" + (tickets--) + "张票");
                }
            }
        }
    }
}
---------------------------------------------------------------------------------------------------------------------------------------
public class MyTest {
    public static void main(String[] args) {
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        MyThread th3 = new MyThread();
        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}

线程安全问题的产生原因分析

  • 首先想为什么出现问题?(也是我们判断是否有问题的标准)
    是否是多线程环境
    是否有共享数据
    是否有多条语句操作共享数据

  • 如何解决多线程安全问题呢?
    基本思想:让程序没有安全问题的环境。
    怎么实现呢?
    把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

    • 判断一个多线程应用程序是否有问题的标准:
    • a: 是否是多线程环境
    • b: 是否存在共享数据
    • c: 是否存在多条语句同时操作共享数据
    • 我们现在这个程序是存在问题的,因为它满足上面的标准,那么我们只要将这个标准打乱,那么我们就可以解决这个问题.
    • 而上面的标准中a , b是不能打乱的,因此我们只能对c做处理,关键是怎么处理? 如果我们把操作共享数据的多条语句看做
    • 成一个整体,当一个线程执行这个整体的时候,其他的线程处于等待状态,也就说当一个线程执行这个整体的时候,其他线程
    • 不能进行执行,那么怎么做到这个一点呢?
    • 需要使用同步代码块:
    • 格式:
      synchronized(对象){//不能在括号了直接new 对象 new 了 就没效果
      要被同步的代码 ;
      }
    • 这个同步代码块保证数据的安全性的一个主要因素就是这个对象
      注意这个对象 要定义为静态成员变量 才能被所有线程共享
    • 需要这个对象被所有的线程对象所共享
    • 这个对象其实就是一把锁.
    • 这个对象习惯叫做监视器

同步代码块的方式解决线程安全问题及解释以及同步的特点及好处和弊端

  • 同步代码块的格式
    格式:
    synchronized(对象){
    需要同步的代码;
    }
    同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能
    B:案例演示: 同步代码块的方式解决线程安全问题
    C:案例解释: 再次给学生解释一遍如何解决了线程安全问题
    D:同步的好处: 同步的出现解决了多线程的安全问题。
    E:同步的弊端: 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,
因为synchronized关键字涉及到锁的概念,所以先来了解一些相关的锁知识。
java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。
线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。
获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,
当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,
直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,
但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,
类锁是用于类的静态方法或者一个类的class对象上的。
我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,
所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,
它只是用来帮助我们理解锁定实例方法和静态方法的区别的.

实现Runnable接口加锁的方式卖电影票

  • 案例演示
    需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
    通过实现Runnable接口实现
public class MyRun implements Runnable {
    int tickets = 100;
    static Object obj = new Object();

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        while (tickets > 0) {
            synchronized (obj) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(tickets>=1){
                    System.out.println(name + "正在出售第" + (tickets--) + "票");
                }

           }
        }
    }
}
---------------------------------------------------------------------------------------
public class SellTickets {
    public static void main(String[] args) {
        MyRun run = new MyRun();
        Thread th1 = new Thread(run);
        Thread th2 = new Thread(run);
        Thread th3 = new Thread(run);
        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}

JDK5之后的Lock锁的概述

  • Lock锁的概述
    虽然我们可以理解同步代码块和同步方法的锁对象问题,
    但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
    为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
  • Lock和ReentrantLock
    void lock()
    void unlock()

死锁问题概述和使用

  • 死锁问题概述
    如果出现了同步嵌套,就容易产生死锁问题
    是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
  • 同步代码块的嵌套案例
    死锁: 两个或者两个以上的线程,在抢占CPU的执行权的时候,都处于等待状态
多执行几次,肯定会出现死锁情况
public class MyThread extends Thread{
    static Object obj1=new Object();
    static Object obj2=new Object();
    boolean flag;
    public MyThread(boolean flag) {
        this.flag=flag;
    }

    @Override
    public void run() {
        String s = Thread.currentThread().getName();
        if(flag){

            synchronized (obj1) {
                System.out.println(s + "执行了1");
                synchronized (obj2) {
                    System.out.println(s + "执行了2");
                }
            }
        }else {
            synchronized (obj2) {
                System.out.println(s+"执行了1");
                synchronized (obj1) {
                    System.out.println(s + "执行了2");
                }
            }
        }

    }
}
------------------------------------------------------------------------------
public class Test {
    public static void main(String[] args) {
        MyThread th1 = new MyThread(true);
        th1.setName("对象1");
        MyThread th2 = new MyThread(false);
        th2.setName("对象2");
        th1.start();
        th2.start();
    }
}

你可能感兴趣的:(Java-多线程、同步代码块解决线程安全问题)