(面试)操作系统-----进程和线程

操作系统定义

操作系统是一个管理软件,对下管理硬件设备,对上给软件提供稳定的运行环境

 冯诺依曼体系结构:CPU中央处理器(运算器+控制器),存储结构,输入设备(键盘,鼠标,输出设备(显示器,音响)

进程和线程定义

进程:(任务资源管理器)一个跑起来的程序,就叫进程,进程是操作系统资源分配的基本单位,进程可以理解为一个应用程序程序执行过程,应用程序一旦执行,就是一个进程,每个进程都有自己独立的地址空间,每次启动一个进程,系统就会为它分配地址空间。

线程:线程被包含在进程中,一个进程默认有一个线程也可以有多个,每一个线程都是一个执行流,可以单独在CPU上进行调度。同一个进程中的·线程公用一份系统资源(内存+文件),所以线程也叫做轻量级进程。线程是调度执行的基本单位

 进程在操作系统中(组织+描述)组织:通过双向链表把PCB串在一起 描述:(pid:身份标识符,内存指针:指向说内存是哪些  ,文件描述符表:硬盘上文件等其他资源)

通过一组PCB(进程控制块)来描述一个进程,每个PCB对应一个线程,一组进程上的内存指针和文件描述符表只有一份,而状态,优先级,上下文则是每个线程都有一份

进程和线程区别 

 1.进程是系统资源分配的基本单位,线程是调度执行的基本单位

 2.不同进程都有自己独立的内存空间,同一个进程所有线程共享本进程的地址空间1

 3.同一个进程所有线程共享本进程资源,进程之间是资源独立

 4.线程创建和销毁开销小,可以提高效率

 5.线程之间会相互干扰,进程之间不会(当线程数目达到极限,CPU核心就会被吃满,如果某个线程发生意外,很可能整个进程会被带走)

线程的创建方式

  • 继承Thread类

public class MyThread extends Thread{//Thread相当于对操作系统中线程进行封装
    @Override
    public void run(){//重写run,run是Thread父类里面已经有的方法,run里面的逻辑就是这个线程要执行的工作
        System.out.println("重写run");
    }
     public static void main(String[] args) {
         MyThread myThread=new MyThread();//创建一个实例,并不是在系统中真创建
         myThread.start();//调用start方法时,才真正的创建了一个线程
     }
}

运行一个Java程序,就是启动一个进程,一个进程里面至少会含有一个线程,main方法所在的线程叫做主线程

main主线程和MyThread创建出的线程都是并发执行的关系

myThread.start另外启动一个线程来执行run方法,新线程是一个单独的执行流,和现有的线程执行流不想关,并发执行

  • 实现Runable接口

class MyRunnableer implements Runnable{
    @Override
    public void run(){
        System.out.println("重写run方法");
    }
}
public class MyRunnable {

    public static void main(String[] args) {
        MyRunnableer myRunnableer=new MyRunnableer();
        Thread t=new Thread(myRunnableer);
        t.start();
    }
}

把线程干的活与线程本身分开,使用myRunnableeer来表示线程要完成的工作,把任务提取出来,目的是为了解耦合

继承Thread写法把线程要完成的工作和线程本身耦合在一起,如果对代码改动比较大,myRunnableeer只需要把它传给其他实体即可

  • 使用匿名内部类,实现创建Thread子类方式 

public static void main(String[] args) {
        Thread t=new Thread(){ //创建Thread子类,同时实例化出一个对象
            @Override
            public void run() {
                System.out.println("重写run方法");
            }
        };
        t.start();
    }
  •  使用匿名内部类完成Runnable

public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {  //匿名内部类实例作为构造方法参数
            @Override
            public void run() {
                System.out.println("重写run");
            }
        });
        t.start();
    }
  •  使用lambda表达式

public static void main(String[] args) {
        Thread t4 = new Thread(() -> {
            System.out.println("任务");
        });

 lambda本质上是匿名函数,()是函数形参  {}是函数体  ->特殊语法

 线程的属性

  • ID

 这个是Java中给Thread对象安排的身份标识符,身份标识符可以有多个,不同环境下使用不同标识符     getid()

  • isDaemon (是不是守护线程)   setDaemon()设置成后台线程,必须在start之前设置

默认创建线程是前台线程,前台线程会阻止进程退出,如果main运行完,前台线程还未完成,进程不会退出,如果是后台线程,则相反

例:转账,必须是前台线程,必须等到所有转账都转完才结束,十分精准

微信运动步数,不精准,后台线程 

  • isAlive (内核线程是否存活)

Thread对象虽然和内核线程是一 一对应关系,但是生命周期并不是完全相同,Thread对象创建出来1,内核线程不一定会有,调用start方法内核线程才会有

当内核线程(run)执行完,内核线程也就销毁了,但Thread对象还在(Thread对象生命周期比线程长) 

  • start() ,调用start方法才会创建线程

 直接调用run并没有创建线程,只是在原来的线程中运行代码

调用start则是创建了线,在新线程中执行代码,和原来线程并发执行

  • 线程中断 本质上是让run方法尽快结束,而不是run执行一半强制结束  

 1.定义一个标志位,作为线程是否结束的标记 

 2.使用标准库自带的标准位     

interrupt方法行为有两种情况:

1.线程在运行状态,会设置            Thread.currentThread().isInterrupted()

2.线程阻塞状态,interrupt会设置标志位,但是sleep/wait这些阻塞方法会清除这个标志位,所以看起来好像没设置,触发interruptException

 在Java中,中断线程不是强制性的,可以由代码本身来决定是立即结束  、还是不理会   、还是稍后处理

public class ThreadDemo {
    private static class MyRunnable implements Runnable{
        @Override
        public void run(){
            while (!Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName()+"1111");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()+"2222");
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName()+"3333");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable=new MyRunnable();
        Thread thread=new Thread(myRunnable,"李四");
        System.out.println(Thread.currentThread().getName()+"李四开始转账");
        thread.start();
        Thread.sleep(10*1000);
        System.out.println(Thread.currentThread().getName()+"李四是骗子");
        thread.interrupt();
    }
}
  •  线程等待(线程之间的调度顺序是不确定的,在main中调用t.jion方法,让main阻塞等待,等执行到t执行完,main才可以继续执行

 public void join(long millis, int nanos)  带时间版本,等待但不是无限等待

  • 获取当前线程引用 (哪个线程调用,得到的就是哪个线程引用)

public class ThreadDemo {  

 public static void main(String[] args) {      

 Thread thread = Thread.currentThread();        哪个线程调用,得到的就是哪个线程引用

System.out.println(thread.getName());   } } 

线程状态

  • NEW: 安排了工作, 还未开始行动(Thread对象创建出来,内核PCB还未创建,没有真正创建线程)

  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.(就绪状态)

  • BLOCKED: 这几个都表示排队等着其他事情,等待锁的时候进入阻塞状态

  • WAITING: 这几个都表示排队等着其他事情,特殊阻塞,调用wait

  • TIMED_WAITING: 按照一定时间进行阻塞

 线程安全

  • 定义:在多线程各种随机调度顺序下,代码没有bug,都能符合预期执行,这样的代码是线程安全的。

  • 线程不安全原因

1.抢占式随机执行 (每个线程调度执行的过程可视为全随机的)

2.多个线程修改同一个变量(string是不可变对象,不能修改string对象内容)

3.修改操作不是原子的(原子表示不可分割的最小单位)

   例:count++操作(三条指令在CPU上完成)

   (1)把内存数据读取到cpu寄存器中 

     (2)把cpu寄存器的值进行+1

      (3) 把寄存器的值放入内存中

cpu执行指令都是以一个指令为单位去执行,不能说指令执行一半就完成线程调度,int赋值则是更安全点

4.内存可见性    可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到(Jvm代码优化引入Bug)

while(counter.count==0){

}

每次执行load,并且load结果还不一样,干脆只读一次,后面不读了(编译器优化)

解决:编译器自己判断不准的,把不该优化的进行了优化需要让程序员显示提醒编译器,这个地方要不要优化volatile

5.指令重排序

解决线程不安全 

  • 加锁(通过特殊手段,让代码变成原子的)

例:在count++之前加锁,count++之后在解锁,这样别的线程只能阻塞等待

public synchronized void increase(){
        count++;
    }
synchronized修饰方法,当进入这个方法就会加锁,方法执行完毕自然解锁
  • 解决内存可见性问题(volatile) 

volatile可以使用这个关键字来修饰一个变量,被修饰的变量,编译器不会出现只读寄存器不读内存的优化 

volatile可保证内存可见性,但不保证原子性(只能针对一个线程读,一个线程使用的情况)

cpu操作寄存器比操作内存快千倍,导致操作内存是一个不明智的选择,重复读内存,不需要真的读内存,只读一次内存,后续读缓存中的数据即可(编译器优化)

代码在写入 volatile 修饰的变量的时候, 改变线程工作内存(寄存器+缓存)中volatile变量副本的值 ,将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候, 从主内存中读取volatile变量的最新值到线程的工作内存中 ,从工作内存中读取volatile变量的副本

加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

import java.util.Scanner;

public class Counter {
  static class Counter1{
      public volatile int a=0;  如果不加volitail,t1读的是工作内存内容,t2对flag变量进行修改,t1也不知道数据是谁,不能立即结束循环
  }

    public static void main(String[] args) {
        Counter1 counter1=new Counter1();
        Thread t1=new Thread(() ->{
            while (counter1.a==0){

            }
            System.out.println("循环结束");
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("输入一个整数");
            counter1.a= scanner.nextInt();
        });
        t1.start();
        t2.start();
    }

 synchronized的几种用法

1.修饰方法(直接修饰方法,相当于锁对象就是this)

public class SynchronizedDemo {    

      public synchronized void methond() {  

} }

2.修饰代码块(有些代码要加锁,有些不需要加锁)要判断锁对象

锁当前对象

public class SynchronizedDemo {  

         public void method() {      

                 synchronized (this) {                  

}   } }

锁类对象

public class SynchronizedDemo {  

         public void method() {      

                 synchronized (class) {                  

}   } }

3.修饰静态方法

public class SynchronizedDemo {  

      public synchronized static void method() {  

} }

1.加锁要考虑好锁哪段代码,锁的范围不一样,代码的执行效果也不一样

 2.不是加锁一定安全,而是通过加锁让并发修改1同一个变量改成串行修改同一个变量

3.如果一个线程上锁,一个没有,也就不存在锁竞争,也就不会阻塞等待,也就不会并发修改变成串行修改

3.在类里面设置静态成员变量,类属性是唯一的,类对象也是唯一的,尽管count和count1是两个实例,但这个静态成员变量其实是一个,会产生锁竞争

  • synchronized的特性

 1.互斥性(synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,如果其他线程也执行到同一个对象,synchroonized就会阻塞等待)

2.可重入(synchronized不会出现两次加锁锁死的情况)

不可重入锁:第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁.

可重入锁:

public class Counter {
    public int count=0;
    synchronized void increase(){
        count++;
    }
    synchronized  void increase2(){
        increase();
    }
}


synchromized(this){ //真正加锁

   synchronized(this){   //直接放行
 
} //执行到这里不用真解锁

}

引用一个计数器,加锁++ 解锁就--  若计数器为0,加锁操作是真加锁

increase和increase2都加锁,increase是针对当前对象进行加锁,调用increase2的时候先加了一次锁,执行到increase又加了一次锁,相当于连续加了两次锁

可重入锁底层实现是非常简单的,只要让锁记录好是哪个线程持有的这把锁就好

t线程尝试针对this来加锁,this这个锁就记录了是t线程有的它,第二次进行加锁时,锁看见还是t线程就直接通过了,不会阻塞

可重入锁:1.让锁里面持有线程对象,记录谁加了锁

                  2.用计数器来判断啥时候加锁和解锁

  • wait和notify用来调配线程执行顺序

  wait, notify, notifyAll 都是 Object 类的方法,Object是所有类的老大,因此可以使用任意类进行实例化,都可以调用wait方法 

 1.线程执行到wait就会阻塞,直到另一个线程调用notify,才可以把这个wait唤醒,继续执行

 2.wait操作本质上做3件事:

    释放当前锁 (释放锁就是给别的线程机会来拿到锁)

    进行等待通知(前提一定是先释放锁) 

    满足一定条件的时候(别人调用notify)被唤醒,然后尝试重新获取锁

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常

 public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        synchronized (object){
            System.out.println("等待中");
            object.wait();                程序会一直等下去,需要notify唤醒
            System.out.println("等待结束");
        }
    }

1.notify也要包含在synchronized里面,线程1没有释放锁,线程2无法调用notify(因为阻塞等待),线程1调用wait,释放了锁,线程1代码阻塞在synchronized里面,但此时锁是释放状态,线程2可以拿到锁 

public class Counter {
  static class Counter1{
      public volatile int a=0;
  }
  static class waitTask implements Runnable{
      private Object locker;
      public waitTask(Object locker){
          this.locker=locker;
      }
      @Override
      public void run(){
          synchronized (locker){
              try {
                  System.out.println("wait开始");
                  locker.wait();
                  System.out.println("wait结束");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }

    static class notifyTask implements Runnable{
        private Object locker;
        public notifyTask(Object locker){
            this.locker=locker;
        }
        @Override
        public void run(){
            synchronized (locker){
                try {
                    System.out.println("notify开始");
                    locker.wait();
                    System.out.println("notify结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        Thread t1=new Thread(new waitTask(locker));
        Thread t2=new Thread(new notifyTask(locker));
        t1.start();
        Thread.sleep(2000);
        t2.start();
    }

    public static void main2(String[] args) throws InterruptedException {
        Object object=new Object();
        synchronized (object){
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
        }
    }

 notifyall唤醒所有的线程

public class Counter {
  static class Counter1{
      public volatile int a=0;
  }
  static class waitTask implements Runnable{
      private Object locker;
      public waitTask(Object locker){
          this.locker=locker;
      }
      @Override
      public void run(){
          synchronized (locker){
              try {
                  System.out.println("wait开始");
                  locker.wait();
                  System.out.println("wait结束");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }

    static class notifyTask implements Runnable{
        private Object locker;
        public notifyTask(Object locker){
            this.locker=locker;
        }
        @Override
        public void run(){
            synchronized (locker){
                    System.out.println("notify开始");
                    locker.notifyAll();
                    System.out.println("notify结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        Thread t1=new Thread(new waitTask(locker));
        Thread t2=new Thread(new notifyTask(locker));
        Thread t3=new Thread(new notifyTask(locker));
        Thread t4=new Thread(new notifyTask(locker));
        Thread t5=new Thread(new notifyTask(locker));
        t1.start();
        t5.start();
        t3.start();
        t4.start();
        Thread.sleep(1000);
        t2.start();
    }

虽然唤醒所有线程,这些线程需要锁竞争

 wait和sleep的区别(面试题)

 

wait需要搭配synchronized使用,sleep不需要

wait是object的方法,sleep是Thread的静态方法

相同点:都可以让线程放弃执行一段时间

  • 多线程的几种设计模式

  • 单例模式 (单个实例、对象)这个是需求决定的,有些场景要求实例不能有多个,本质上就是借助编程语言的自身语法特性,强行限制某个类,不能创建多个实例

                                                                  饿汉模式

 解决方案:static名义上是静态,实际上起到的效果和名字无任何关系

                   static修饰成员/属性,变成类成员/类属性,当属性变成类对象属性,此时已经是单个实例(类对象通过JVM加载(.class)文件,而此时类对象,其实在JVM中也是实例,JVM对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的成员static修饰也只有一份)

class Singleton{
    private static Singleton instance=new Singleton(); //这个singleton这个类唯一实例,类加载时创建实例
    private Singleton(){} //把构造方法设为private,此时类外无法new实例
    public static Singleton getInstance(){ //拿到singleton实例需要借助getInstaance
        return instance;
    }
}

                                                                 懒汉模式

创建实例更加迟,带来更高效率

class Singleton1{
    private static Singleton1 insatnce=null;
    private Singleton1(){}   
    public static Singleton1 getInstance(){
        if (insatnce==null){
            insatnce=new Singleton1();  真正创建实例
        }
        return insatnce;
    }
}

 这个创建实例是不安全的,如果多个线程同时调用getInsatance方法,就可能创建多个实例

进行优化 

class Singleton1{
    private volatile  static Singleton1 insatnce=null;//禁止指令重排序和内存可见性
    private Singleton1(){}
    public static synchronized Singleton1 getInstance(){  //加锁变成原子操作,t2读的是t1修改后的值
        if (insatnce==null){ //判断是否需要加锁,因为线程加锁开销比较大,线程不安全在实例创建之前,需要加锁,实例创建之后不加
            synchronized (Singleton1.class){ 
                 if(instance==null){  //判断是否要创建实例
                insatnce=new Singleton1();
            }
        }
        return insatnce;
    }
}

1.双重if判定,第一个if判断是否需要加锁(加锁开销比较大,线程不安全只存在与实例创建之前)   第二个if是判断是否需要创建实例

2.假设2个线程同时调用getInsatnce,第一个线程拿到锁,进入第二层if,开始new对象(1.申请内存,得到内存首地址2.调用构造方法,构造实例 3.把内存首地址赋给instance引用),这个场景可能会造成指令重排序,t1执行了1和3之后(得到不完全对象,只是有内存,内存数据无效),执行2在前

  • 阻塞对列 (符合先进先出)

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.

当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素. 

应用场景:生产者消费者模型 (多线程协同工作一种方式)

优点:1.阻塞队列有利于代码解耦合(明确分工,之间并没有过多交集)

           2.相当于缓冲区,平衡了消费者和生产者处理能力(当流量暴增A和队列承受压力,B和C还是按原来节奏消费数据,缓解直接冲击)

  •  线程池

 1.为什么从线程池里面直接取比创建新线程快?

创建线程需要在操作系统内核中实现,涉及用户态---内核态切换操作,存在开销,从线程池里面取、放只涉及用户态,用户每个进程都是自己执行逻辑,效率更高

2.Java标准库直接写好的线程池 

 ExecutorService pool = Executors.newFixedThreadPool(10);

此处创建线程池没有显示new而是通过executors静态方法来完成,这样叫做工厂模式

Java线程池本体是ThreadPoolExactor,构造方法麻烦,简化构造方法,标准库提供一系列工厂方法

 3.线程池使用

 public static void main(String[] args) {
        ExecutorService pool= Executors.newCachedThreadPool();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("helll0");
            }
        });
    }

 4.线程池实现

1.一个线程池的可以同时提交n个任务,对应线程池中有m个线程负责n个任务(生产者消费者模型,先搞一个阻塞对列,每一个任务放在阻塞队列里面,m个线程从队列里面取元素) 

  •  锁策略

乐观锁:预测锁冲突概率不高(在数据提交更新时才会对数据是否产生并发冲突去检测)

悲观锁:预测锁冲突概率较高(每次拿数据都会上锁)

普通互斥锁:synchronized当两个线程竞争同一把锁,就会产生等待

读写锁:把读操作和写操作区分开来(两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.;两个线程都要写一个数据, 有线程安全问题;. 一个线程读另外一个线程写, 也有线程安全问题.)

读写锁相比普通互斥锁,少了锁竞争,优化效率

Synchronized 不是读写锁

 轻量级锁:加锁解锁开销比较小(纯用户态加锁,线程池)

重量级锁:加锁解锁开销比较大(进入内核态加锁,开销大)

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

 自旋锁:轻量级锁(如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会 在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.)

挂起等待锁:重量级锁

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的.

公平锁:有先来后去顺序

非公平锁:不管先来后到顺序

操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.

synchronized 是非公平锁 

可重入锁:针对同一个线程同一把锁,连续加锁两次,不会出现死锁情况

不可重入锁: 针对同一个线程同一把锁,连续加锁两次,会出现死锁情况

synchronized关键字锁都是可重入的

  • CAS

1.定义: 把内存中的某个值和CPU寄存器的值进行交换(如果两个值相同,就把另一个寄存器B中的值和内存值进行交换,把内存值放到寄存器B,同时B值写给内存),一系列操作是通过一个CPU指令完成(原子的),不仅线程安全还高效。

2.应用场景

 1.实现原子类(CAS 是直接读写内存的, 而不是操作寄存器. CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的)

 2.实现自旋锁

  • CAS的ABA问题 

1.在CAS中进行比较时,发现寄存器A和内存M值相同,无法判断M是不是变了又变回来,还是始终都没变

2.大部分情况反复改变是不影响,但也有特殊情况(ABA)扣款两次

3.解决方案:

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期 

如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1. 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

  • Synchronized原理 

 1.Synchronized是加锁,当两个线程针对同一个对象加锁时,会出现锁竞争,之后另一个线程就得阻塞等待,直到这个线程释放锁

2.synchronized的加锁过程

  • 偏向锁(类似与懒汉模式,必要时加锁,能不加就不加,偏向锁不是真的加锁,而是只设置了一个状态,产生锁竞争才会加锁)
  • 轻量级锁(随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应自旋锁)通过CAS来实现,通过 CAS 检查并更新一块内存 (比如 null => 该线程引用) 如果更新成功, 则认为加锁成功 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU),但此处的自旋不会一直持续进行,而是达到一定时间、重复次数就不在自旋,也就是所谓的自适应)
  • 重量级锁(如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁)(执行加锁操作, 先进入内核态. 在内核态判定当前锁是否已经被占用 如果该锁没有占用, 则加锁成功, 并切换回用户态. 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒. 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁.)
  • Callable(是一个接口,描述任务带返回值)

Callable 和 Runnable 相对, 都是描述一个 "任务".

Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务.

Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定. FutureTask 就可以负责这个等待结果出来的工作 

 //创建线程计算1+。。。。100的返回值
    static class Result{
        public int sum=0;
        public Object lock=new Object();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer>callable=new Callable<Integer>() {  //写一个匿名内部类实现callable接口
            @Override
            public Integer call() throws Exception { //重写callable的call方法完成累加
                int sum=0;
                for (int i = 1; i <=1000 ; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask=new FutureTask<>(callable); //把Callable实例使用FutureTask包装一下
        Thread t=new Thread(futureTask);//创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
        t.start();
        int result=futureTask.get();//在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
        System.out.println(result);
    }
  •  JUC(java.util.concurrent的常见类)

1.ReentrantLock (对synchronized的补充)

lock.lock();  
try {    
 // working    
} finally {    
 lock.unlock()    
}  

 优势:

1.trylock提供更多的可能(试试看可以加锁,成功则加锁成功,失败则加锁失败,并且还可以指定加锁等待超时时间

2.synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式   RuntrantLock locker=new Runtrantlock(true)

3.. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指 定的线程

4.synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, 在 JVM 外实现的(基于Java实现的)

2.Semaphore(信号量)用来表示 "可用资源的个数". 本质上就是一个计数器

 public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(4); 初始化为4,表示有4个可用资源
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    semaphore.acquire();                acquire方法申请资源p,可用资源减一
                    System.out.println("我获取到资源");
                    Thread.sleep(1000);
                    System.out.println("释放资源");
                    semaphore.release();                release释放资源,可用资源+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 20; i++) {  创建20个线程,每个线程尝试申请资源,sleep1秒后,释放资源
            Thread t=new Thread(runnable);
            t.start();
        }

 3.CountDown Latch(类似于比赛)使用它先设置有几个选手,每个选手撞线调用此方法,当撞线次数达到选手个数就比赛结束

public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch=new CountDownLatch(10);//表示有10个任务·
        Runnable r=new Runnable() {
            @Override
            public void run() {
                Thread.sleep(Math.random(10000));
                latch.countDown(); //每个任务执行完毕都调用countDown,在CountDownLatch内部计数器同时自减
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
        //必须等10人全完成任务
        latch.await(); //阻塞等待所有任务执行完毕,计数器为0
        System.out.println("比赛结束");
    }

4.线程池

理解 ThreadPoolExecutor 构造方法的参数:

把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)

maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退). keepAliveTime: 临时工允许的空闲时间.

unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.

workQueue: 传递任务的阻塞队列 threadFactory: 创建线程的工厂, 参与具体的创建线程工作. RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理. AbortPolicy(): 超过负荷, 直接抛出异常.

CallerRunsPolicy(): 调用者负责处理

DiscardOldestPolicy(): 丢弃队列中最老的任务.

DiscardPolicy(): 丢弃新来的任务. 

  • 死锁 

 1.一个线程一把锁,线程连续加锁两次,如果这个锁是不可重入锁,则是死锁

 2.两个线程两把锁 (锁套锁)

 3.两人分别拿不同的东西不交换

 4.多个线程多把锁,更容易死锁(哲学家就餐)

 死锁的4个必要条件:

1.互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2.不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3.请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

4.循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路

解决:给锁编号,给定两把锁之间必须先取编号小的,后取大的,

锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号 (1, 2, 3...M). N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.

public static void main(String[] args) {
        Object lock1=new Object();
        Object lock2=new Object();
        Thread t1=new Thread(){
            @Override
            public void run(){
                synchronized (lock1){
                    synchronized (lock2){

                    }
                }
            }
        };
        t1.start();
        Thread t2=new Thread(){
            @Override
            public void run(){
                synchronized (lock1){
                    synchronized (lock2){

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值