Java面试突击班(抓住金九银十) 持续更新中

文章目录

      • 1.Java中线程的实现方式?
      • 2. Java线程中的状态?
      • 3. Java中如何停止线程?
      • 4. Java中Sleep和wait方法的区别?
      • 5.扩展—— P5典型 P6典型 P7典型
      • 6.并发编程的三大特性
        • 原子性
          • 什么时并发编程得原子性
          • 保证并发编程得原子性
        • 可见性
          • 什么时可见性
          • 解决可见性的方式
        • 有序性
          • 什么是有序性
      • 7.什么是CAS,有什么优缺点
      • 8.Contended注解的作用

1.Java中线程的实现方式?

(1)继承Thread类,重写run方法。
启动线程是调用start()方法,这样会创建一个新的线程,并执行线程任务。
如果直接调用run()方法,这样会让当前线程执行run()方法中的业务逻辑。
代码如下:

public class P1 {
    public static void main(String[] args) {
        MyJob myJob = new MyJob();
        myJob.run();
        myJob.start();
        for (int j = 0; j < 2; j++) {
            System.out.println("start:j="+j);
        }
    }
}
class MyJob extends Thread{
    public void run(){
        for (int i = 0; i < 2; i++) {
            System.out.println("run:i="+i);
        }
    }
}

(2) 实现Runnable接口 重写run方法
Thread实现Runnable接口,因此两者追根究底一样。 Java面试突击班(抓住金九银十) 持续更新中_第1张图片
但由于Java中是单继承,所以相比继承Thread类要好一些。
最常用的方式:

  • 匿名内部类:
Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("i=" + i);
                }
            }
        });
  • lambda方式:
  Thread thread = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println("lambda:"+i);
            } 
        });
    }

(3)实现Callable重写call方法,配合FutureTask
Callable一般用于有返回结果的非阻塞的执行方法。
非同步阻塞
如果想要返回一个结果,以上两种是没有办法是现实的。

public class P3 {
    public static void main(String[] args) throws ExecutionException,InterruptedException {
        Mycallables mycallables = new Mycallables();
        FutureTask futureTask = new FutureTask(mycallables);
        Thread thread = new Thread(futureTask);
        thread.start();
        //操作
        //要结果
        Object count = futureTask.get();
        System.out.println(count);
    }
}
class Mycallables implements Callable{

    @Override
    public Object call() throws Exception {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            count +=1 ;
        }
        return count;
    }
}

Java面试突击班(抓住金九银十) 持续更新中_第2张图片
Java面试突击班(抓住金九银十) 持续更新中_第3张图片

(4) 基于线程池构建线程
追其底层,其实只有一只,就是实现Runnable接口

2. Java线程中的状态?

5种(一般针对操作系统层面):新建状态—start—就绪状态—(CPU调度)—运行状态—(wait()、sleep()、join())—等待状态——结束状态。
Java中给线程准备的6种状态:新建——运行\就绪状态(runnable)——阻塞状态(blocked)——等待(手动唤醒wating)——时间等待(timed_waiting)——结束
NEW:

//New
    public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(()->{
	
        	});
   	 System.out.println(t1.getState());
}
 // Runnable 就绪/运行状态
     public static void main(String[] args) throws InterruptedException {
       Thread t2 = new Thread(()->{
           while(true){

           }
        });
        t2.start();
        Thread.sleep(500);
        System.out.println(t2.getState());
}
    //Blocked
     public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t3 = new Thread(()->{
            //t3下拿不到锁资源,导致变成Blocked状态
            synchronized (object){

            }
        });
        //main线程拿到obj的锁资源
         synchronized (object){
            t3.start();
            Thread.sleep(500);
            System.out.println(t3.getState());
         }
      }
      public static void main(String[] args) throws InterruptedException {
        // Waiting
        Object object0 = new Object();
        Thread t4 = new Thread(()->{
            synchronized (object0){
                try {
                    object0.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t4.start();
        Thread.sleep(500);
        System.out.println(t4.getState());
       }
  // Timed_Waiting
     public static void main(String[] args) throws InterruptedException {
        Object object1 = new Object();
        Thread t5 = new Thread(()->{
            synchronized (object1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t5.start();
        Thread.sleep(500);
        System.out.println(t5.getState());
   }

3. Java中如何停止线程?

(1)stop方法(不推荐,但却是可以做到。过时了)
强制让线程结束的方法有很多,最常用的就是让run方法结束,如论是return结束,还是抛出异常结束,都可以

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(()->{
		try{
			Thread.sleep(5000);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	});
	t1.strat();
	Thread.sleep(5000);
	t1.stop();
	System.out.println(t1.getState());
}

(2)使用共享变量(很少会用)
这种方式用的不多,有的线程可能会通过死循环来保证一直运行。
咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法

 static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(flag){
                // 处理任务
            }
            System.out.println("任务结束");
        });
        t1.start();
        Thread.sleep(500);
        flag = false;

(3)interrupt方式
休眠时也可以停掉

/*=============interrupt================*/
        //中断标记位 默认形况下 为false
        System.out.println(Thread.currentThread().isInterrupted());
        // 执行interrupt之后,再次查看打断信息
        Thread.currentThread().interrupt();
        // interrupt标记位:true
        System.out.println(Thread.currentThread().isInterrupted());
        // 返回当前线程,并归位为false interrupt标记位:true
        System.out.println(Thread.interrupted());
        // 已经归位了
        System.out.println(Thread.interrupted());

        //=======================================
        Thread t3 = new Thread(()->{
           while(!Thread.currentThread().isInterrupted()){
               // 处理业务
           }
            System.out.println("end");
        });
        t3.start();
        Thread.sleep(500);
        t3.interrupt();

4. Java中Sleep和wait方法的区别?

  • sleep属于Thread类中的static方法,wait 属于Object类的方法
  • sleep属于Timed_waiting,自动被唤醒;wait属于waiting,需要手动唤醒
  • sleep方法在持有锁时,执行,不会释放锁资源;wait在执行后,会释放锁资源
  • sleep方法在持有锁或者不持有锁时,执行;wait方法必须在持有锁时才可以被执行
    wait方法会将持有锁的线程从owner仍到WaitSet集合中,这个操作实在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。

5.扩展—— P5典型 P6典型 P7典型

P5典型:解释一下什么是乐观锁、悲观锁、自旋锁、读写锁、排他锁、共享锁、统一锁、分段锁、行锁、表锁等。

  • 乐观锁:首先来看乐观锁,顾名思义,乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。比如数据库提供的类似于write_condition机制,Java API 并发工具包下面的原子变量类就是使用了乐观锁的CAS来实现的。
    适用场景:它适用于写少读多的情况,也就是说减少操作冲突,这样可以省去锁竞争的开销,提高系统的吞吐量。
    坏事未必会发生,事后补偿
  • 悲观锁 :悲观锁就是持悲观态度的锁。就在操作数据时比较悲观,每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。比如行锁、表锁、读锁、写锁,都是在操作之前先上锁,Java API中的synchronized和ReentrantLock等独占锁都是悲观锁思想的实现。
    适用场景:它适用于写多读少的情况。因为,如果还使用乐观锁的话,会经常出现操作冲突,这样会导致应用层会不断地Retry,反而会降低系统性能。
    坏事一定会发生,预先预防
  • 自旋锁:一种常见的乐观锁实现,CS锁
    • ABA问题(看似值没有改变,其实已经经过改变如:0-8-0)(解决方法:加版本< version、boolean >)
long sequence ;经历一个操作就要++一次
public class TestAtomicStampedReference {
    private static class Order {
        long sequence;
        long time;

        @Override
        public String toString() {
            return "Order{" +
                    "sequence=" + sequence +
                    ", time=" + time +
                    '}';
        }
    }

//    static AtomicStampedReference orderRef = new AtomicStampedReference<>(new Order(), 0);
    static AtomicMarkableReference<Order> orderRef = new AtomicMarkableReference<>(new Order(), false);

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                Order old = orderRef.getReference();
//                int stamp = orderRef.getStamp();

                Order o = new Order();
                o.sequence = old.sequence + 1;
               o.time = System.currentTimeMillis();

//               orderRef.compareAndSet(old,o,stamp,stamp+1);
                orderRef.compareAndSet(old,o,false,true);

            }).start();
        }
//            SleepHelper.sleepSeconds(1);
        System.out.println(orderRef.getReference());

    }
}

- 保障CAS操作的原则性问题(lock问题)
  • 排他锁:只有一个线程能访问代码
  • 共享锁:可以允许有多个线程访问代码
  • 读写锁
    - 读锁:读的时候,不允许写,但是允许同时读
    - 写锁:写的时候,不允许写,也不允许读(排他锁)
  • 统一锁:大粒度的锁
    - 锁定A等待B,锁定B等待A
    - A+B统一起来成为大锁 解决死锁问题
  • 分段锁:分成一段一段小粒度的锁
  • 间隙锁:是一个在 索引记录 之间的间隙上的锁

P6典型

  • 对于线程池的理解和运用
  • 如何理解线程池的7大参数?
ThreadPoolExecutor tpe = new ThreadPoolExecutor(2,4,60, 
                TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

Java面试突击班(抓住金九银十) 持续更新中_第4张图片
1.corePoolSize: 核心线程数的大小
2.maximunPoolSize:最大线程数大小
3.keepAliveTime:生存时间
4. TimeUnit.SECONDS:生存时间的单位
5.ArrayBlockingQueue(4):任务队列(核心)
6.Executors.defaultThreadFactory():线程池产生的工厂(区分不同的线程抛出的不同异常)
7.RejectedExecutionHandler handler:拒绝策略
估计并发量,确定核心线程数,核心线程永远存在。如餐厅服务员,两个核心员工,消息队列长度为四个,当两个客户占用两个核心员工之后,则从外部调用临时员工(临时员工+核心员工=最大员工数),临时员工服务接下来的客户,之后再服务队列中的客户。(规则由程序来定)。《此中,员工对应线程,客户对应任务》
设置线程的数量能把cpu占满,是跟该线程的IO密集和CPU密集息息相关的。
当线程数全满,又有任务进来的时候,不能够丢掉的任务,可以对其进行持久化。默认有四个策略(jdk)。服务外面可以搭一个mq的集群,扔进mq中,当线程空闲,再拿出来。
有界队列:永远使用有界
无界:如链表队列,尽量不使用

P7典型

  • 什么是纤程/协程
  • 它和普通的Java线程有什么不同
  • 为什么它能提高效率
扩充一下:什么是线程、进程?
一个程序-可执行文件,一般程序在硬盘上,想执行时,从硬盘放入内存里,这是这个程序就是一个进程(为其分配进程空间)。若要运行起来,则需要在进程空间找到线程入口-主线程。
线程是一般称为程序运行的基本单元。
一个CPU一般跑一个线程
线程撕裂者/超线程

6.并发编程的三大特性

原子性

JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定的差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。
让Java的并发编程可以做到跨平台。
JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定)

什么时并发编程得原子性

原子性得定义:一个操作是不可分割得,不可中断得,一个线程再执行时,另一个线程不会影响到他.

保证并发编程得原子性

synchronized:可以在方法上追加synchronized关键字或者采用同步代码块得形式来保证原子性,synchronized可以让避免多线程同时操作临界资源,同一时间点,只会有一个线程正在操作临界资源。
CAS:compare and swap也就是比较和交换,他是一条CPU的并发原语。
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。
Lock锁
Lock锁在1.5之前,性能相比较synchronized好,1.6之后,synchronized做了优化,性能相差就不大了。如果涉及并发比较多时,推荐ReentrantLock,性能会更好。其底层主要通过lock和unlock来实现。
ThreadLocal
其实,原子性用ThreadLocal很难保证,因为一般原子性是为了避免多线程使用共享变量,从而带来线程不安全。
ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据。

可见性

什么时可见性

可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对于CPU来说,去内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会储存到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
这就会带来问题,现在二点CPU都是多核的,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己额工作内存,没有及时的同步到主内存,导致数据不一致问题。

解决可见性的方式

volatile:修饰成员变量,使用该关键字,相当于告知CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主存内操作
volatile的内存语义:
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置无效,必须去主内存中重新读取共享变量。
加了volatile修饰的属性,会在转为汇编之后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
- 将当前处理器缓存行的数据写回到主内存
- 这个写回的数据,在其他的CPU内核的缓存中,直接无效
synchronized:加锁的时候将数据,同步到主内存
Lock:与synchronized完全不同,synchronized是基于他的内存语义,在获取和释放锁时,对CPU缓存做一个同步到主内存的操作;而lock锁是基于volatile实现的,Lock锁内部再进行枷锁和释放时,会对一个由volatile修饰和state属性进行加减操作。
final:final和volatile不允许同时修饰一个属性,final修饰的属性不允许再被写了。

有序性

什么是有序性

在Java中,.java文件中的内容会被编译,再执行前需要再次转为CPU可以识别的指令,CPU再执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满需一些要求),会对指令进行重排。
指令乱序执行的原因,是为了尽可能的发挥CPU的性能。
as-if-serial:
happens-before:
程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
线程启动规则(Thread Start Rule):Thread对象start()方法先行发生于此线程的每一个动作。
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法和Thread.isAlive()的返回值等手段检测线程是否已经终止执行。
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数结束)先行发生于它的finalize()方法的开始。
传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
volatile:

7.什么是CAS,有什么优缺点

CAS:compare and swap也就是比较和交换,他是一条CPU的并发原语。
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。
缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。
AtomicStampedReference在CAS时,不但会判断原值,还会比较版本信息。

 public static void main(String[] args) {
        AtomicStampedReference<String>reference = new AtomicStampedReference<>("AAA",1);
        String oldValue = reference.getReference();
        int oldVersion = reference.getStamp();
        boolean b = reference.compareAndSet(oldValue,"B",oldVersion,oldVersion+1);
        System.out.println("修改版本1的:"+b);
        boolean c = reference.compareAndSet("b","c",1,1+1);
        System.out.println("修改版本2的:"+c);
    }

自旋时间过长问题:
- 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
- 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。

8.Contended注解的作用

你可能感兴趣的:(Java,面试系列,java,面试)