多线程

多线程

进程

概念

是一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。

线程

概念

就是进程的一个独立的控制单元。线程在控制着进程的执行。

进程和线程关系

一个进程中至少有一个线程。

Java VM 启动的时候会有一个进程java.exe

该进程中至少一个线程负责Java程序的执行,而且这个线程运行的代码存在与main方法中,该线程称为主线程。

扩展

其实更细节说明jvmjvm启动不止一个线程,还负责垃圾回收机制的线程。

多线程存在的意义

多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

线程的创建方式

创建线程方式一:继承Thread类

继承Thread类

  1. 子类覆盖父类中的run方法,将线程运行的代码存放在run中。
  2. 建立子类对象的同时线程也被创建。
  3. 通过调用start方法开启线程。
class Demo extends Thread {
    @Override
    public void run() {
        System.out.println("demo run");
    }
}

public class Main {

    public static void main(String[] args) {
        Demo d = new Demo();
        //start方法:使该线程开始执行,java虚拟机调用线程的run方法。
        // 输入:demo run
        d.start();
    }
}

发现运行结果每一次都不同。

因为多个线程都获取cpu执行权限,cpu执行到谁,谁就运行。

明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)

cpu在做着快速的切换,以达到看上去是同时运行的效果。

我们可以形象把多线程的运行行为在互相抢夺cpu执行权限。

这就是多线程的一个特性,随机性。谁抢到谁执行,至于执行多长,cpu说的算。

class Demo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println("demo run" + i);
        }
    }
}

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            System.out.println("main run" + i);
        }
        Demo d = new Demo();
        d.start();
    }
}

线程状态

start(): 开始执行线程。

sleep():需要指定睡眠时间,单位是毫秒。一个特殊的状态:就绪。具备了执行资格,但是还没有获取资源

wait():在其他线程调用此对象的 notify() 方法或 notifyAll()方法前,导致当前线程等待。

notify(): 唤醒在此对象监视器上等待的单个线程。

stop(): 已过时 强迫线程停止执行。该方法具有固有的不安全性。

创建线程方法二:实现Runnable接口

  1. 定义类实现Runnable接口。
  2. 覆盖Runnable接口的子类
  3. 通过Thread类建立线程对象。
  4. Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
    1. 为什么要将Runnable接口的子类对象传递给Thread的构造函数。
      1. 因为自定义的run方法所属的对象是Runnable接口子类对象,所以要让线程去指定对象的run方法。就必须明确该run方法所属对象。
  5. 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

实现方法和继承方式有什么区别?

实现方法好处:避免了单继承的局限性。

在定义线程时,建立使用实现方法。

class Ticket implements Runnable {
    private int tick = 100;

    @Override
    public void run() {
        while (true) {
            if (tick > 0) {
                System.out.println(Thread.currentThread().getName() + " = " + tick--);
            }
        }
    }
}

public class Main {

    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        t1.start();
        Thread t2 = new Thread(t);
        t2.start();
        Thread t3 = new Thread(t);
        t3.start();
    }
}

线程安全问题

导致安全问题的出现的原因:

  • 多个线程访问出现延迟。
  • 线程随机性。

注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。

例如下方的代码:

可能会导致数据重复,或者数据超出。

class Ticket implements Runnable {
    private int tick = 100;

    @Override
    public void run() {
        while (true) {
            if (tick > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " = " + tick--);
            }
        }
    }
}

public class Main {

    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        t1.start();
        Thread t2 = new Thread(t);
        t2.start();
        Thread t3 = new Thread(t);
        t3.start();
    }
}

同步(synchronized)

格式:

// 写法一:同步代码块
synchronized (对象) {
    需要同步的代码;
}
// 写法二:同步函数
public synchronized void fun() {
    需要同步的代码;
}

同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁功能。

对象如同锁。持有锁的线程可以在同步中执行。

没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

// 同步就不会出现安全问题
class Ticket implements Runnable {
    private int tick = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tick > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " = " + tick--);
                }
            }
        }
    }
}

public class Main {

    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        t1.start();
        Thread t2 = new Thread(t);
        t2.start();
        Thread t3 = new Thread(t);
        t3.start();
    }
}

同步的前提:

  1. 必须要有两个或者两个以上的线程。
  2. 必须是多个线程使用同一个锁。

必须保证同步中只能有一个线程在运行。

好处:解决了多线程的安全问题。

弊端:多个线程需要判断锁,较为消耗资源,

如何在多线程中找出问题并加上同步:

  1. 明确哪些代码是多线程运行代码。
  2. 明确共享数据。
  3. 明确多线程运行代码中哪些语句操作共享数据的。
没有同步
/*需求:
银行有一个金库。
有两个储户分别存300员,每次存100,存3次。

目的:该程序是否有安全问题,如果有,如何解决?*/
class Bank {
    private int sum;
    private Object obj  = new Object();

    public void add(int n) {
        synchronized (obj) {
            sum += n;
            System.out.println("sum = " + sum);
        }
    }
}

class Cus implements Runnable {
    private Bank b = new Bank();

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            b.add(100);
        }
    }
}

public class BankDemo {
    public static void main(String[] args) {
        Cus c = new Cus();

        Thread t = new Thread(c);
        t.start();
        Thread t2 = new Thread(c);
        t2.start();
    }
}

没有同步就会导致数据异常。

sum = 100
sum = 300
sum = 400
sum = 200
sum = 500
sum = 600
同步
/*需求:
银行有一个金库。
有两个储户分别存300员,每次存100,存3次。

目的:该程序是否有安全问题,如果有,如何解决?*/
class Bank {
    private int sum;
    private Object obj  = new Object();

    public void add(int n) {
        synchronized (obj) {
            sum += n;
            System.out.println("sum = " + sum);
        }
    }
}

class Cus implements Runnable {
    private Bank b = new Bank();

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            b.add(100);
        }
    }
}

public class BankDemo {
    public static void main(String[] args) {
        Cus c = new Cus();

        Thread t = new Thread(c);
        t.start();
        Thread t2 = new Thread(c);
        t2.start();
    }
}

输入的数据正常

sum = 100
sum = 200
sum = 300
sum = 400
sum = 500
sum = 600

多线程通信

其实就是多个线程在操作同一个资源,但是操作的动作不同。

class Res
{
    String name;
    String sex;
    boolean flag = false;
}

class Input implements Runnable
{
    private Res r ;
    Input(Res r)
    {
        this.r = r;
    }
    public void run()
    {
        int x = 0;
        while(true)
        {
            synchronized(r)
            {

                if(r.flag)
                    try{r.wait();}catch(Exception e){}
                if(x==0)
                {
                    r.name="mike";
                    r.sex="man";
                }
                else
                {
                    r.name="丽丽";
                    r.sex = "女女女女女";
                }
                x = (x+1)%2;
                r.flag = true;
                r.notify();
            }
        }
    }
}

class Output implements Runnable
{
    private Res r ;

    Output(Res r)
    {
        this.r = r;
    }
    public void run()
    {
        while(true)
        {
            synchronized(r)
            {
                if(!r.flag)
                    try{r.wait();}catch(Exception e){}
                System.out.println(r.name+"...."+r.sex);
                r.flag = false;
                r.notify();
            }
        }
    }
}


class  InputOutputDemo
{
    public static void main(String[] args) 
    {
        Res r = new Res();

        Input in = new Input(r);
        Output out = new Output(r);

        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);

        t1.start();
        t2.start();
    }
}

wait();
notify();
notifyAll();

都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。

思考1:为什么这些操作线程的方法要定义Object类中呢?

因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义Object类中。

思考2:wait(),sleep()有什么区别?

  • wait():释放cpu执行权,释放锁。
  • sleep():释放cpu执行权,不释放锁。