Java高级--->多线程的学习

1.进程

1.进程是正在运行的程序,也就是执行程序的一次执行过程,是系统进行资源分配的基本单位
2.目前操作系统都是支持多进程,可以进行执行多个进程,通过进程ID区分
3.单核cpu在同一时刻,只能有一个进程,宏观并行,微观串行。

2. 线程

线程,又称轻量级进程,进程中的一条执行路径,也是cpu的基本调度单位。一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程。
例如:迅雷是一个进程,当中的多个下载任务即为线程。
Java虚拟机是一个继承,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行

3.进程和线程的区别

1.进程是操作系统资源分配的基本单位,而线程是cpu的基本调度单位。
2.一个程序运行后至少有一个进程。
3.一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
4.进程间不能共享数据段地址,但是同进程的线程之间可以。

4.线程的组成

任何一个线程都具有基本的组成部分

      CPU时间片: 操作系统(OS)会为每个线程分配执行时间  

       运行数据:
            堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。
            栈空间:  存储线程需使用的局部变量,每个线程都拥有独立的栈。
       
       线程的逻辑代码. 

5.线程的特点

1. 线程抢占式执行
		效率高 
 		可防止单一线程长时间独占CPU.
2. 在单核CPU中,宏观上同时执行,微观上顺序执行

6.线程的创建方式(三种)

6.1 第一种方式: 继承Thread类 重写run方法

在这里插入图片描述

    //1.继承线程
public class MyThread extends Thread{

    //2。重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"==="+i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //3.创建线程
        MyThread myThread = new MyThread();
        myThread.setName("aaa");
        //4.开启线程
        myThread.start();
    }
}

相关方法:

一、获取线程ID和线程名称
	1.在Thread的子类中调用this.getId()或this.getName()
	2.使用Thread.currentThread().getId()和Thread.currentThread().getName()
二、修改线程名称
	1. 调用线程对象的setName()方法
	2. 使用线程子类的构造方法赋值

举例:使用线程Thread类实现4个窗口各卖100张票

public class MyThread extends Thread{
    int ticket=100;
    @Override
    public void run() {
        for(;ticket>0;ticket--){
            System.out.println(Thread.currentThread().getName()+"剩余:"+ticket+"张票");
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        m1.setName("A窗口");
        m1.start();
        MyThread m2 = new MyThread();
        m2.setName("B窗口");
        m2.start();
        MyThread m3 = new MyThread();
        m3.setName("C窗口");
        m3.start();
        MyThread m4 = new MyThread();
        m4.setName("D窗口");
        m4.start();

    }
}

若想让四个窗口共卖这100张票可在ticket前加 static 即static int ticket=100;
在这里插入图片描述

6.2 第二种方式: 实现Runnable接口

在这里插入图片描述
实例:实现四个窗口共卖100张票

public class MyRunnable implements Runnable{
    int ticket=100;
    @Override
    public void run() {
        for(;ticket>0;ticket--){
            System.out.println(Thread.currentThread().getName()+"剩余:"+ticket+"张票");
        }
    }
}
public class TestDemo {
    public static void main(String[] args) {
    	//可以将MyRunnable理解为一个任务由四个窗口执行,则共享票数 从而实现四个窗口
    	//共卖100张票
        MyRunnable r1 = new MyRunnable();
        Thread t1 = new Thread(r1,"A窗口");
        Thread t2 = new Thread(r1,"B窗口");
        Thread t3 = new Thread(r1,"C窗口");
        Thread t4 = new Thread(r1,"D窗口");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
6.3 第二种方式: 实现Callable接口

实现Callable接口,它和实现Runnable接口差不多,只是该接口种的方法有返回值和异常抛出。

public class Test {
    public static void main(String[] args) throws Exception {
        My1 task1=new My1();
        My2 task2=new My2();

        //第一种方式建创建线程对象并提交Callable类型的任务
        //但是这种方式是比较麻烦的,需要封装到FutureTask类种,
        //因此建议使用线程池来提交任务
        FutureTask futureTask=new FutureTask(task1);

        Thread t1=new Thread(futureTask);
        t1.start();
        System.out.println(futureTask.get());

        //第二种方式 使用线程池来提交任务     应用场景: 适合大文件上传。
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        Future<Integer> future = executorService.submit(task1);
        Integer sum1 = future.get();//需要等线程执行完毕后,才会把结果返回给该变量

        //实现1-100的和
        Future<Integer> future1 = executorService.submit(task2);
        Integer sum2 = future1.get();

        System.out.println(sum1+sum2);


    }
}
class My1 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i=1;i<=50;i++){
            sum+=i;
        }
        return sum;
    }
}

class My2 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i=51;i<=100;i++){
            sum+=i;
        }
        return sum;
    }
}

7.线程的生命周期以及状态转换

在这里插入图片描述

在这里插入图片描述
       1.新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new Thread();

       2.就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

       3.运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

       4.阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。

根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞--运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的 sleep() 或 join() 或发出了I/O请求时,线程会进入到阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

       5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期;

8.线程的常用方法

  1. 休眠:

     public static void sleep(long millis)
     当前线程主动休眠millis毫秒。
    
public class ThreadSleep {
    public static void main(String[] args) {
        ThreadSleepDemo t = new ThreadSleepDemo();
        t.start();
        for(int i=1;i<10;i++){
            System.out.println("main线程====循环"+i+"次");
        }
    }
}

class ThreadSleepDemo extends Thread{
    @Override
    public void run() {
        for(int i=1;i<10;i++){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");
        }
    }
}

  1. 放弃:

     public static void yield()
     当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片	
    
    
     yield()只让有相同执行权的线程获得cup时间片,
     但是yield()不能控制cup交出的时间,
     yeild()只是让线程恢复到就绪状态,
     那么可能在执行yeild()后进入就绪状态,然后马上又进入运行状态。
    
public class ThreadYield {
    public static void main(String[] args) {
        TY t1 = new TY();
        t1.start();

        for(int i=1;i<10;i++){
            Thread.yield();
            System.out.println("main线程===循环"+i+"次");
        }
    }
}
class TY extends Thread{
    @Override
    public void run() {
        for(int i=1;i<10;i++){
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");
        }
    }
}

3.加入:

 	 public final void join()
  	 允许其他线程加入到当前线程中

	在main函数线程中调用线程tj.join()方法,此时main函数线程就进入阻塞状态,
	直到线程tj完全执行完以后,线程main才结束阻塞状态
public class ThreadJoin {
    public static void main(String[] args) {
        TJ tj = new TJ();
        tj.start();
        try {
        //tj执行完后才会执行main函数
            tj.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=1;i<10;i++){
            System.out.println("main线程===循环"+i+"次");
        }
    }
}
class TJ extends Thread{
    @Override
    public void run() {
        for(int i=1;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");
        }
    }
}

4.优先级:

	 线程对象.setPriority()
 	 线程优先级1-10,默认为5,优先级越高,表示获取CPU的概率越高

5.守护线程:

 	线程对象.setDaemon(true);设置为守护线程。
 	线程有两类:用户线程(前台线程)和守护线程(后台线程)
 	如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。
 	垃圾回收线程属于守护线程。
public class ThreadDeamon {
    public static void main(String[] args) {
        TP tp1 = new TP();
        tp1.setName("A线程");
        //当main线程执行完毕后,后台TP线程自动结束
        tp1.setDaemon(true);
        tp1.start();

        for(int i=1;i<20;i++){
            System.out.println("main线程===循环"+i+"次");
        }
    }
}
class TP extends Thread{
    @Override
    public void run() {
        for(int i=1;i<50;i++){
            System.out.println(Thread.currentThread().getName()+"===循环"+i+"次");
        }
    }
}

9.线程的安全问题

在这里插入图片描述

public class TestSafe {
    //静态资源 共享
    private static String [] arr=new String[5];
    private static int index=0;
    public static void main(String[] args) throws Exception {
        //匿名对象--
        Runnable hello=new Runnable() {
            @Override
            public void run() {
                if (arr[index] == null) {
                     arr[index] = "hello";
                     index++;
                }
            }
        };
        Runnable world=new Runnable() {
            @Override
            public void run() {
              if (arr[index] == null) {
                  arr[index] = "world";
                  index++;
                }
            }
        };
        Thread t1=new Thread(hello);
        Thread t2=new Thread(world);

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


        t1.join();
        t2.join();
        System.out.println(Arrays.asList(arr));
        //可能出现的情况,所以出现的情况会丢失 即数据不安全 
        // (1)hello world null null null
        // (2)world hello null null null
        // (3)hello null null null null
        // (4)world null null null null
    }
}

多线程安全问题:
1.当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
2.临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
3.原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

使用synchronized可以解决线程不安全问题

synchronized语法:

synchronized(临界资源对象){//对临界资源对象加锁
	//代码 原子操作
}
public class TestSafe {
    //静态资源 共享
    private static String [] arr=new String[5];
    private static int index=0;
    public static void main(String[] args) throws Exception {
        //匿名对象--
        Runnable hello=new Runnable() {
            @Override
            public void run() {
                //加锁 可以解决安全问题
                synchronized (arr) {
                    //共享资源 原子操作
                    if (arr[index] == null) {
                        arr[index] = "hello";
                        index++;
                    }
                }
            }
        };
        Runnable world=new Runnable() {
            @Override
            public void run() {
                synchronized (arr) {
                    if (arr[index] == null) {
                        arr[index] = "world";
                        index++;
                    }
                }
            }
        };
        Thread t1=new Thread(hello);
        Thread t2=new Thread(world);

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

        t1.join();
        t2.join();
        System.out.println(Arrays.asList(arr));
    }

运行一直是这个
在这里插入图片描述

注意:
1.每个对象都有一个互斥锁标记,用来分配给线程
2.只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步代码块
3.线程退出同步代码块时,会释放相应的互斥锁标记

10.线程死锁

当A线程拥有锁资源a时,这时A线程需要锁资源b, 而B线程拥有锁资源b,这时B线程需要锁资源a, 这样会导致A等待B线程释放资源b, B线程等待A线程释放锁资源a。 从而二个处于永久等待。从而操作死锁。

例子: 两人去餐厅吃饭,A有一根筷子,B有另一个筷子。A要等B的那根筷子,B要等A的筷子,两个人都在循环等待,则会陷入死锁状态。

解决办法:使用synchronized可以解决该问题

public class Boy extends Thread{
    @Override
    public void run() {
        synchronized (LockObject.b){
            System.out.println(Thread.currentThread().getName()+"获得筷子b");
            synchronized (LockObject.a){
                System.out.println(Thread.currentThread().getName()+"获得筷子a");
            }
        }
    }
}
public class Gril extends Thread{
    @Override
    public void run() {
        synchronized (LockObject.a){
            System.out.println(Thread.currentThread().getName()+"获得筷子a");
            synchronized (LockObject.b){
                System.out.println(Thread.currentThread().getName()+"获得筷子b");
            }
        }
    }
}
public class LockObject {
    /*筷子a*/
    public static final Object a = new Object();
    /*筷子b*/
    public static final Object b = new Object();
}
public class Test {
    public static void main(String[] args) {
        Boy boy = new Boy();
        boy.setName("aaa");
        Gril gril = new Gril();
        gril.setName("bbb");
        boy.start();
        gril.start();
    }
}

1.得到死锁的原因:锁与锁之间有嵌套导致。

2.解决死锁的方法:

1. 尽量减少锁得嵌套。
2. 可以使用一些安全类。
3. 可以使用Lock中得枷锁,设置枷锁时间。

11.线程通信

所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

因为我们无法对哪个线程先获得cpu,也无法确定哪个线程先执行,但是我们想指定某个线程先执行,哪些线程后执行,这样的话我们就需要线程通信技术。

线程通信中的方法:

1.等待 -- 释放锁,进入等待队列
	public final void wait()
	public finnal void wait(long timeout)
	必须在对obj加锁的同步代码块中。
	在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有的锁标记。同时此线程阻塞让其在等待队列中
	属于Object类中的方法

2.通知 -- 唤醒等待队列中的线程,进入就绪队列中,参与cpu的竞争
	public final void notify()
	public final void notifyAll()

实例:存钱和取钱
在这里插入图片描述
规定-- 先存钱在取钱 且存一次钱 取一次钱

public class BankCard {
    private double balance;
    private boolean flag=false;

    public double getBalance() {
        return balance;
    }

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

    //存钱
    public synchronized void save(double money){
        //若还有取钱则让该线程等待 进入等待队列
        if(flag==true){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则 如下操作
        this.balance += money;
        System.out.println(Thread.currentThread().getName()+"存了"+money+", 余额为:"+this.balance);
        //存钱后将flag置于true 进入取钱操作
        flag=true;
        //唤醒当前等待队列中线程
        this.notify();
    }

    //取钱 加入同步代码 使得线程安全
    public synchronized void withdraw(double money){
        //若还没有存钱 则让该线程等待
        if(flag==false){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则 如下操作
        this.balance -= money;
        System.out.println(Thread.currentThread().getName()+"取了"+money+", 余额为:"+this.balance);
        //取钱后将flag置于false 进入存钱操作
        flag=false;
        //唤醒当前等待队列中线程
        this.notify();
    }
}
public class BoyCard implements Runnable{
    private  BankCard bankCard;

    public BoyCard(BankCard bankCard) {
        this.bankCard = bankCard;
    }

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            bankCard.save(1000);
        }
    }
}

public class GrilCard implements Runnable{
    private  BankCard bankCard;

    public GrilCard(BankCard bankCard) {
        this.bankCard = bankCard;
    }

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            bankCard.withdraw(1000);
        }

    }
}
public class Test {
    public static void main(String[] args) {
        BankCard bankCard = new BankCard();
        BoyCard boyCard = new BoyCard(bankCard);
        GrilCard grilCard = new GrilCard(bankCard);
        Thread t1  = new Thread(boyCard,"AA");
        Thread t2  = new Thread(grilCard,"BB");
        t1.start();
        t2.start();

    }
}
sleep和wait的区别:
	1.所在的类不同:sleep属于Thread类,wait属于Object类。
	2.使用的地方: sleep可以在任何代码块中使用。wait只能在同步代码块中使用。
	3.锁资源的释放: sleep不释放锁资源,wait会释放锁资源。
	4.sleep时间片到了自动唤醒,wait必须需要使用notify或notifyAll唤醒

notify()和 notifyAll()有什么区别?
	1.notifyl()会唤醒所有的线程,notify()会唤醒一个线程。
	2.notifyAll() 会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

12.线程池

该池子中预先存储若干个线程对象。整个池子就是线程池

存在问题:
	1.线程是宝贵的内存资源,单个线程约占1MB的空间,过多分配易造成内存溢出。
	2.频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。

线程池的作用:
	1.线程容器,可设定线程分配的数量上限
	2.将预先创建的线程对象存入池子中,并重用线程池中的线程对象
	3.避免频繁的创建和销毁。

线程池的创建方式:

所有的线程池—封装了一个父接口—java.util.concurrent.Executor.

​ 它的实现接口: ExecutorService.

工具类Executors可以创建相应的线程池:

[1] 创建单一线程池 newSingleThreadExecutor()

[2] 创建定长线程池。newFixedThreadPool(n);

[3] 创建可变线程池. newCachedThreadPool()

[4] 创建延迟线程池 .newScheduledThreadPool(n);

方法:
	Executor:线程池的根类.它中的方法execute()执行线程任务的方法Runnable类型的任务
    ExecutorService: 线程池的子接口
    shutdown(); 关闭线程池。需要等待线程池中任务执行完毕后才会关闭。
    shutdownNow(): 立即关闭线程池。
    isTerminated():判断线程池是否终止了。
    submit(): 提交任务给线程池中线程对象、Runnable和Callable类型的任务。
  1. 创建单一线程池 newSingleThreadExecutor()
    适应场景:队列要求线程有序执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 1; i <= 5; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "====");
                }
            });
            executorService.shutdown();
        }

不管存在几个线程 永远都是一个线程在执行五次

  1. 创建定长线程池。newFixedThreadPool(n);
ExecutorService executorService = Executors.newFixedThreadPool(2);
        for(int i=1;i<=5;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "====" );
                }
            });
            executorService.shutdown();
        }
  1. 创建可变线程池. newCachedThreadPool()
ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i=1;i<=5;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "====" );
                }
            });
            executorService.shutdown();
        }
  1. 创建延迟线程池 .newScheduledThreadPool(n);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                for(int i=1;i<=5;i++){
                    System.out.println(Thread.currentThread().getName()+"===="+i);
                }
            }
        },10, TimeUnit.SECONDS);
        scheduledExecutorService.shutdown();
        }
	//long delay:延迟时间 TimeUnit unit:时间单位,Runnable command:runnable的创建线程
 public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);

推荐使用原始的创建线程池方式
上面使用Executors创建线程池的四种放方式,都是基于底层ThreadPoolExecutor实现,而阿里开发手册,建议使用最原始的方式。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险

        /*原始*/
        /*int corePoolSize, 核心线程数
*         int maximumPoolSize, 最大线程数
*         long keepAliveTime, 空闲时间
*         TimeUnit unit, 时间单位
*         BlockingQueue<Runnable> workQueue: 堵塞队列,
*         LinkedBlockingDeque:可以设置等待的个数,如果不设置默认为Integer的最大值。*/

        LinkedBlockingDeque blockingDeque = new LinkedBlockingDeque(3);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 10, TimeUnit.SECONDS, blockingDeque);
        /*这里注意 要保证循环的次数-阻塞队列线程数<= 最大线程数 否则存在异常*/
        for (int i = 0; i < 8; i++) {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"===");
                }
            });
        }
        //关闭线程
        executor.shutdown();
    }

13.手动锁

Lock是手动锁的父接口,它下面有很多实现类。
lock() :获取锁,如锁被占用,则等待
unlock()释放锁资源,放在finally中
boolean tryLock():尝试获取锁(成功返回true,失败返回false,不阻塞)
在这里插入图片描述
使用重入锁解决卖票

public class demo04 {
    public static void main(String[] args) {
        Ticket task=new Ticket();

        Thread t1=new Thread(task,"窗口A");
        Thread t2=new Thread(task,"窗口B");
        Thread t3=new Thread(task,"窗口C");
        Thread t4=new Thread(task,"窗口D");

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

class Ticket implements  Runnable{
    private int ticket=100;
    Lock s=new ReentrantLock();
    @Override
    public void run() {
        while(true) {
            try {
                s.lock();//查看释放获取锁资源
                if (ticket > 0) {
                    --ticket;
                    System.out.println(Thread.currentThread().getName() + "卖了一张,剩余:" + ticket + "张");
                } else {
                    break;
                }
            }finally {
                s.unlock();//释放锁
            }
        }
    }
}
Synchronized和Lock区别:
1.synchronized可以给类,方法,代码块加锁,而lock只能给代码块加锁
2.synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而lock需要自己加锁和释放锁,如果使用不当没有unlock()去释放锁就会造成死锁
3.通过lock可以知道有没有成功的获取锁,而synchronized却无法办法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值