java基础笔记06 - 多线程


六 多线程

1.基本概念

? 程序:一组指令的集合,一段静态的代码

? 进程:程序的一次执行过程,或者正在运行的一个程序,是一个动态的工作恒,有自身的产生,存在和消亡的过程--------生命周期

? 程序是静态的的,晋城市动态的

? 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径

? 线程作为调度的最小单位,每个线程拥有独立的运行栈(VM Stack)和程序计数器(pc) 线程的切换开销小

? 多个线程共享进程的资源(方法区和堆),所以访问的是相同的变量和对象

? 一个java.exe,至少三个线程:main线程, gc垃圾回收线程,异常处理线程

2. 线程的创建使用

2.1 方式1

2.1.1 创建

? -创建一个继承于Thread类的子类

? -重写Thread类的run方法

? -创建子类对象

? -调用子类对象的start()方法 ,只能start一次

import java.lang.Thread;
public Test extends Thread{
    @override
    public void run(){
    	//将此线程执行的操作声明在run()中
        for(int i = 0; i<100;i++){
            if(i %2==0){
                sout(i);
            }
        }
    }
}
main(){
    Test t= new Test();
    t.start();  //start会启动线程,然后调用run方法
}

2.1.2省略方式(匿名对象)

public static void main(String[] args){
	new Thread(){
		@override
		public void run(){
			.....
		}
	}.start();
}

2.1.3创建三个窗口卖100张票

注意使用static来解决线程间共享资源的问题,但是目前的代码还没做线程安全处理

public class Ly extends Thread {
    private static final   int ticket = 100;
    private static int num=0;
    public Ly(String name) {
        super(name);
    }

    public void run() {
        while (true) {
            if (num < ticket) {
                num++;
                System.out.println(getName() + ":买票,票号为:" + num);

            } else {
                break;
            }
        }
    }

    public static void main(String[] args) {
        new Ly("窗口1").start();
        new Ly("窗口2").start();
        new Ly("窗口3").start();
    }
}

2.3 Thread类的常见方法

? void start():启动当前线程, 调用run方法

? void run():需要重写,将线程需要执行的操作,卸载run里面

? String getName();

? void setName();//在start方法之前设置

? static Thread currentThread();返回当前代码执行时的线程

? public Thread(String s);//Thread类的构造器,可以直接起名,构造方法挺多的,这个是线程名参数的

? yield( );//让步,释放当前线程的cpu的执行(可能紧接着cpu又分给它了,没准的事)

? join(); // 把cpu切换到当前线程,直到其执行完才结束阻塞!!!

? stop();//强制结束,干掉

? sleep(millis);//单位毫秒,需要try-catch,阻塞当前线程

? isAlive();//判断当前线程还活着么

2.4线程调度

? 时间片,抢占式

? 同优先级的线程,先到先得,使用时间片策略

? 高优先级使用抢占式策略

2.4.1线程优先级

MAX_PRIORITY=10;
MIN_PRIORITY=1;
NORM_PRIORITY=5; //默认优先级

2.4.2获取和设置线程优先级

在start之前设置吧,最好

getPriority();
setPriority(int);

2.5 方式2

2.5.1创建

? -创建一个实现了Runnable接口的类

? -实现类去实现Runnable中的抽象方法run()

? -创建实现类的对象

? -将此对象作为参数传递到Thread类的构造方法中,创建Thread类的对象

? -通过Thread类的对象调用start()方法

public class Ly implements Runnable {
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            if (i%2==0) {
                System.out.println(i);
            }
        }
    }
    public static void main(String[] args) {
        Thread th=new Thread(new Ly());
        th.start();
    }
}

? Q:调用的是Thread类里面的run,为啥跑的是Ly实现类的run

? A:Thread里面声明了一个Runnable类型的target成员

? 同时,Thread有一个构造器:Thread(Runnable target),传入target之后,会调用target的run

2.5.2创建三个窗口卖100张票

? 注意,只创建了一个Runnable实现类的对象,然后三个线程就共用一个资源了

? 一样,先忽略线程安全问题

public class Ly implements Runnable {
    private int ticket = 10;
    private int num = 0;
    @Override
    public void run() {
        while (true) {
            if (num < ticket) {
                num++;
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + num);
            } else {
                break;
            }
        }
    }
    public static void main(String[] args) {
        Ly ly = new Ly(); //注意,只有一个Runnable的实现类
        Thread th1 = new Thread(ly);
        th1.setName("窗口1");
        th1.start();
        Thread th2 = new Thread(ly);
        th2.setName("窗口2");
        th2.start();
        Thread th3 = new Thread(ly);
        th3.setName("窗口3");
        th3.start();
    }
}

2.6两种创建线程方式的对比

别问,问就是用Runnable方式
实际开发,线程类可能需要有继承关系,就不能继承Thread类了,为了避免多继承,提供了Runnable接口
同时,Runnable天然提供了线程资源共享功能;

3. 线程的生命周期

3.1 线程的几种状态

? NEW,新建 Thread state for a thread which has not yet started.

? RUNNABLE:运行 executing in the Java virtual machine but it may be waiting for other resources from the operating system such as

? BLOCKED:阻塞,在队列里等待cpu (原因:调用sleep了,join其他线程了,等待同步锁,wait(), suspend过时了,因为导致死锁

? 阻塞之后会回到WAITING就绪状态

? WAITING:调了start方法 等待cpu时间片,或者是yield的了

? TIMED_WAITING:

? TERMINATED:terminated死亡了(运行完了,stop了,异常了)

? 就直接记住 新建,就绪,运行,阻塞,死亡,五种就行

4.线程的同步

4.1最初的两种方式

4.1.1 方式1:同步代码块

把操作共享数据的代码程序需要同步的代码

注意:

? 1.不能包多了,也不能包少了,少了导致线程不安全,多了可能导致某个线程霸占资源

? 2. 对于某一个临界资源来说,锁只能放一把,而在第一种继承的方式创建多线程时,注意锁别声明多了,(继承方式天然生成多个类)

synchronized(同步监视器){ //俗称锁,任何一个类的对象都能充当锁,放只狗都行,但是!!只能是放一只狗,两只就不行
	//需要被同步的代码	
}
import javax.security.auth.kerberos.KerberosTicket;
public class Test implements Runnable {
    private int ticket = 10;
    Object obj = new Object();

    public void run() {
        while (true) {
            synchronized (obj) { //加把锁 ,用this也行,或者Test.class(因为类也是对象)
                //个人猜测,就是想放一个唯一的标识符,甚至怀疑他这里用的对象的hash值
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":买票,票号:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        Test test=new Test();
        Thread th1=new Thread(test);
        Thread th2=new Thread(test);
        Thread th3=new Thread(test);
        th1.start();
        th2.start();
        th3.start();
    }
}

缺点:操作同步代码时,只能有一个线程参与,退化成单线程了,速度变慢了

4.1.2 方式2:同步方法

如果临界资源涉及到的代码正好完整的声明在了一个方法中,则将该方法声明为同步方法

其实还是有个同步监视器,只不过没有显式 的声明了

非静态的同步方法,锁是this

静态的同步方法,锁是类本身

实现接口的多继承方式:

import javax.security.auth.kerberos.KerberosTicket;

/**
 * @author
 * @create 2021-11-25 21:40
 */
public class Test implements Runnable {
    private int ticket = 10;
    Object obj = new Object();

    public void run() {
        while (true) {
            sailticket();
        }
    }

    private synchronized void  sailticket(){
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":买票,票号:" + ticket);
            ticket--;
        } else {
            break;
        }
    }
    public static void main(String[] args) {
        Test test=new Test();
        Thread th1=new Thread(test);
        Thread th2=new Thread(test);
        th1.start(); 
        th2.start();
    }
}

继承的多线程方式

同步方法要设置成静态的,.

别问,问就是不用


4.1.3 懒汉式设计模式的改写

使用同步机制,将单例模式中的懒汉式改写为线程安全的 (多个线程同时创建一个单例

import javax.security.auth.kerberos.KerberosTicket;
public class Test implements Runnable {
    public Bank bank;
    public void run(){
        this.bank=Bank.getInstance();
        System.out.println(bank);
    }

    public static void main(String[] args) {
        Test test=new Test();
        Thread th1=new Thread(test);
        Thread th2=new Thread(test);
        th1.start();
        th2.start();
    }
}

class Bank{
    private Bank(){}
    private static Bank instance=null; //instance算是一个临界资源
     //方式1
    public static  Bank getInstance1(){
        if (instance==null){
            synchronzed(Bank.class){
                if (instance==null){
           	 		instance=new Bank();
        		}
            }
        }
        return instance; 
    }
    //方式2
    public static synchronzed Bank getInstance2(){
    	if (instance==null){
        	instance=new Bank();
         }
    	 return instance; 
    }
}

4.1.3死锁问题

俩匿名的线程类,一个继承实现,一个接口实现,这俩抢两个临界资源,造成死锁

public class Test {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();
        new Thread(){
            public void run(){
                synchronized (s1){
                    try {
                        sleep(100); //sleep是为了方便演示死锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("a");
                    s2.append("1");
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("c");
                    s2.append("3");
                    synchronized (s1)   {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

4.2 方式3 :Lock锁

老规矩,继承方式实现的时候lock必须是static的,保证锁是唯一的

import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
public class Test implements Runnable {
    private int ticket = 10;
    //step1:实例化一个Lock
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try{
                //step2:调用lock
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally {
               //step3:调用unlock
               lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        Test test = new Test();
        Thread th1 = new Thread(test);
        Thread th2 = new Thread(test);
        th1.setName("窗口1");
        th2.setName("窗口2");
        th1.start();
        th2.start();
    }
}
举例存钱
import java.util.concurrent.locks.ReentrantLock;
class Account{
    private int count=0;
    private ReentrantLock lock=new ReentrantLock();
    public void add(int amt) {
        if(amt>0){
            try{
                lock.lock();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count+=amt;
                System.out.println(Thread.currentThread().getName()+" 存钱成功,余额为:"+count);
            }finally {
                lock.unlock();
            }

        }
    }
}
class AccountTest implements Runnable{
    private Account account;
    public AccountTest(Account account){
        this.account=account;
    }
    @Override
    public void run() {
        for(int i=0;i<3;i++){
            account.add(1000);
        }
    }
}
public class Test  {
    public static void main(String[] args) {
        AccountTest test=new AccountTest(new Account());
        Thread C1=new Thread(test);
        Thread C2=new Thread(test);
        C1.setName("wang");
        C2.setName("zhang");
        C1.start();
        C2.start();
    }
}

4.3 synchronized 和lock的差别

? Lock需要手动释放锁,

? synchronized自动释放,但是要在执行完{ }之后自动释放

? 基本老的用的都是synchronized,新的都建议用lock

? 别想了 synchronized多难写啊。。。。。

5. 线程的通信

5.1 线程交易阻塞、唤醒

举个例子:俩线程,交替打印123456.....

wait()方法:当前线程进入阻塞状态,并释放锁

notify()方法:唤醒wait的线程,如果有很多个,释放优先级最高的

notifyAll()方法:唤醒所有的阻塞线程

注意:

  1. 这三个方法必须使用在同步代码块或者同步方法中, lock方法的后面再讲
  2. 这三个方法调用者必须是锁,下例中是this,并且省略了
  3. 这三个方法不是定义在Thread或者Runnable中的,而是Object中
public  class Test implements Runnable{
    private int number=1;
    @Override
    public void run() {
        while (true){
            synchronized () {
                notify();//唤醒方法
                //notifyAll();//唤醒所有方法
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(number<=10){
                    System.out.println(Thread.currentThread().getName()+"打印:"+number);
                    number++;
                    try {
                        wait();//阻塞线程,wait方法是会释放锁的,sleep就不会
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
    public static void main(String[] args) {
        Test test=new Test();
        Thread th1=new Thread(test);
        Thread th2=new Thread(test);
        th1.setName("线程1");
        th2.setName("线程2");
        th1.start();
        th2.start();
    }
}

5.2 经典题:生产者消费者问题

生产者Productor生产产品,交给店员Clerk,消费者Customer从店员处取产品,店员处产品存放有上限

注意:完全不加wait和notify,也没事,但是浪费线程资源,以生产者为例,即便满了

public class ProductorTest {
    public static void main(String[] args) {
        Clert clert=new Clert(); //这意味着锁是唯一的
        Productor thp=new Productor(clert);
        Thread p1=new Thread(thp);
        p1.setName("生产者1");

        Customer thc=new Customer(clert);
        Thread c1=new Thread(thc);
        Thread c2=new Thread(thc);
        c1.setName("消费者1");
        //c2.setName("消费者2");
        p1.start();
        c1.start();
       // c2.start();

    }
}
class Productor implements Runnable {
    private Clert clert;
    public Productor(Clert clert) {
        this.clert = clert;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {e.printStackTrace();}
            clert.product();
        }
    }
    public void product(){

    }
}

class Customer implements Runnable {
    private Clert clert;
    public Customer(Clert clert) {
        this.clert = clert;
    }
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();}
            clert.custom();
        }
    }
}
class Clert{
    private int number=0;
    //生产产品
    public synchronized void product() {
        if(number<10){
            number++;
            notify();
            System.out.println(Thread.currentThread().getName()+"++产品,库存:"+number);

        }else {
            try {
                wait();
            } catch (InterruptedException e) { e.printStackTrace();}
        }
    }
    //消费产品
    public synchronized void custom() {
            if(number>0){
                number--;
                System.out.println(Thread.currentThread().getName()+"消耗产品,库存:"+number);
                notify();
            }else{
                try {
                    wait();
                } catch (InterruptedException e) {e.printStackTrace();}
            }
        }
}

6. JDK5.0 新增线程创建方式

6.1 实现Callable接口

6.1.1 对比Runnable,Callable

  1. Callable接口重写call方法
  2. 相比run()方法,call()方法有返回值
  3. 支持泛型得返回值
  4. run只能使用try catch,call方法可以抛出异常
  5. 需要借助FutureTask类 (他实现了Runnable和Callable接口)

6.1.2 实现

为啥这么麻烦了,因为要兼容老版本啊,那群傻逼死活就想用老系统有啥办法

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//step1:创建实现Callable接口得实现类
public class Test implements Callable {
    //step2:将线程需要执行得操作放在call方法中,重写
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for(int i=1;i<=10;i++){
                sum+=i;
        }
        return sum;
    }

    public static void main(String[] args) {
        //step3:创建Callable实现类的对象
        Test test=new Test();
        //step4:借助FutureTask类,将Callable对象传入其中
        FutureTask task =new FutureTask(test);
        //step5 : 类似于Runnable方法,将FutureTask的对象传递给Thread类,再start
        new Thread(task).start();
        try {
            //step6:使用FutureTask类对象得方法get获取线程run的返回值
            Object value = task.get(); //get方法得返回值就是Callable对象得call方法得返回值
            //如果对call方法得返回值不感兴趣,则不调用get方法也行
            System.out.println(value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

6.2 使用线程池 (真正用的)

死麻烦死麻烦的,谁家想弄个线程还得声明三步变量啊(Callable一个,FutureTask一个,Thread一个)

实际开发都用线程池,是不是一瞬间觉得前面全白学了 ????

6.2.1 概念

提前创建好多个线程,放进线程池中,用的时候直接捞,用完了再扔回去,避免频繁的创建销毁

优点:

  1. 提高响应速度,因为不用创建了

  2. 降低了资源消耗,因为重复利用线程

  3. 便于线程管理

    corePoolSize: //核心池得大小
    maximumPoolSize://最大线程数
    keepAliveTime://线程再没有任务时最多保持多长时间就终止
    

6.2.2 使用

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

        ExecutorService service = Executors.newFixedThreadPool(10);
        //设置线程池得属性,ExecutorService是个接口,所以不能直接用service设置
        ThreadPoolExecutor se= (ThreadPoolExecutor)service;
        se.setCorePoolSize(15);
        
        service.execute(new NumberThread1());//适合使用于Runnable
        service.submit(new NumberThread2());//适合使用于Callable
        service.shutdown(); //不加线程不结束
    }
}
class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <10; i++) {
            if (i%2==0)
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
class NumberThread2 implements Callable {

    @Override
    public Object call() {
        for (int i = 0; i <10; i++) {
            if (i%2==1)
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
        return null;
    }
}