Java 并发知识点汇总

204 阅读30分钟

几个比较牛逼的并发系列文章:

Java知音

死磕Java并发

小小旭GISer 并发技术

1、使用线程的方式

有三种使用线程的方法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现 Runnable 接口

public class MyRunnable implements Runnable {
    @Override   
    public void run() {
        // ...
    }
}

public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() {
        return 123;
    }
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

继承 Thread 类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

public class MyThread extends Thread {
    @Override
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口。
  2. 类可能只要求可执行就行,继承整个 Thread 类开销过大。
  3. 增强程序的健壮性,代码能够被多个程序共享。

2、线程状态

Java中线程的状态分为6种。

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • 阻塞(BLOCKED):表示线程阻塞于锁。
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。

3、线程的几个常见方法的比较

Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。

Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

thread.join()/thread.join(long millis)当前线程里调用其它线程thread的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程thread执行完毕或者millis时间到,当前线程进入就绪状态。

thread.interrupt(),当前线程里调用其它线程thread的interrupt()方法,中断指定的线程。 如果指定线程调用了wait()方法组或者join方法组在阻塞状态,那么指定线程会抛出InterruptedException。

Thread.interrupted,一定是当前线程调用此方法,检查当前线程是否被设置了中断,该方法会重置当前线程的中断标志,返回当前线程是否被设置了中断。

thread.isInterrupted(),当前线程里调用其它线程thread的isInterrupted()方法,返回指定线程是否被中断。

object.wait()当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。

object.notify() 唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

4、 线程之间的协作

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

join() 在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。

public class JoinExample {

    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}

public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}
A
B

wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

它们都属于 Object 的一部分,而不属于 Thread。

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

public class WaitNotifyExample {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

wait() 和 sleep() 的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

public class AwaitSignalExample {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

5、Java线程池

什么是线程池,如何使用?为什么要使用线程池?

线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程即可,节 省了开辟子线程的时间,提高了代码执行效率。

线程池的优势

  • 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;

  • 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;

  • 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;

ThreadPoolExecutor 我们可以通过ThreadPoolExecutor来创建一个线程池。

ExecutorService service = new ThreadPoolExecutor(....);

下面我们就来看一下ThreadPoolExecutor中的一个构造方法。

public ThreadPoolExecutor(int corePoolSize,
 	int maximumPoolSize,
 	long keepAliveTime,
 	TimeUnit unit,
 	BlockingQueue<Runnable> workQueue,
 	ThreadFactory threadFactory,
 	RejectedExecutionHandler handler) 

ThreadPoolExecutor参数含义

1. corePoolSize

线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。

2. maximumPoolSize

线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。

3. keepAliveTime

非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。

4. unit

用于指定keepAliveTime参数的时间单位。他是一个枚举,可以使用的单位有天(TimeUnit.DAYS),小时(TimeUnit.HOURS),分钟(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS),微秒(TimeUnit.MICROSECONDS, 千分之一毫秒)和毫微秒(TimeUnit.NANOSECONDS, 千分之一微秒);

5. workQueue

线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute方法提交的Runable对象都会存储在该队列中。我们可以选择下面几个阻塞队列。

  • ArrayBlockingQueue :基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。
  • LinkedBlockingQueue:基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。
  • SynchronousQueue:内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间。对于SynchronousQueue中的数据元素只有当我们试着取走的时候才可能存在。
  • PriorityBlockingQueue:具有优先级的无限阻塞队列。

我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。

6. threadFactory

线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。

7. handler

是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。

当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。

在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。

下面是在ThreadPoolExecutor中提供的四个可选值。

  • CallerRunsPolicy 只用调用者所在线程来运行任务。
  • AbortPolicy 直接抛出RejectedExecutionException异常。
  • DiscardPolicy 丢弃掉该任务,不进行处理。
  • DiscardOldestPolicy 丢弃队列里最近的一个任务,并执行当前任务。

我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。

ThreadPoolExecutor的使用

ExecutorService service = new ThreadPoolExecutor(5, 10, 10
           , TimeUnit.SECONDS
           , new LinkedBlockingQueue<>());

对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。

可以通过 execute 和 submit 两种方式来向线程池提交一个任务

execute: 当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。

service.execute(new Runnable() {
	public void run() {
		System.out.println("execute方式");
	}
});

submit: 当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。

Future<Integer> future = service.submit(new Callable<Integer>() {

	@Override
	public Integer call() throws Exception {
		System.out.println("submit方式");
		return 2;
	}
});
try {
	Integer number = future.get();
} catch (ExecutionException e) {
	e.printStackTrace();
}

线程池关闭

调用线程池的shutdown()或shutdownNow()方法来关闭线程池

shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。

中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。 但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。

6、Java中的四种线程池

  • 第一种:newCachedThreadPool,不固定线程数量,这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,意思也就是说来一个任务就创建一个woker,回收时间是60s。

可缓存线程池: 1、线程数无限制。 2、有空闲线程则复用空闲线程,若无空闲线程则新建线程。 3、减少频繁创建/销毁线程,减少系统开销。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());
}
  • 第二种:newFixedThreadPool,固定线程数量,corePoolSize 跟 aximumPoolSize 值一样,同时传入一个无界阻塞队列,该线程池的线程会维持在指定线程数,不会进行回收。

定长线程池: 1、可控制线程最大并发数(同时执行的线程数)。 2、超出的线程会在队列中等待。

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory);
}
  • 第三种:newSingleThreadExecutor,可以理解为线程数量为 1 的 FixedThreadPool,线程池中只有一个线程进行任务执行,其他的都放入阻塞队列,

单线程化的线程池: 1、有且仅有一个工作线程执行任务。 2、所有任务按照指定顺序执行,即遵循队列的入队出队规则。

public static ExecutorService newSingleThreadExecutor() {
    // 外面包装的FinalizableDelegatedExecutorService类实现了finalize方法,在JVM垃圾回收的时候会关闭线程池
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 第四种:newScheduledThreadPool,支持定时以指定周期循环执行任务。注意:前三种线程池是ThreadPoolExecutor不同配置的实例,最后一种是ScheduledThreadPoolExecutor的实例。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

7、Java线程池原理

线程池ThreadPoolExecutor实现原理

从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)和HashSet集合构成。 从任务提交的流程角度来看,对于使用线程池的外部来说,线程池的机制是这样的:

  1. 如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待;
  2. 如果正在运行的线程数 >= coreSize,把该task放入阻塞队列;
  3. 如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task;
  4. 如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。

8、线程池都有哪几种工作队列?

  1. ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

  2. LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。

  3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

  4. PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

9、怎么理解无界队列和有界队列?

  • 有界队列
  1. 初始的poolSize < corePoolSize,提交的runnable任务,会直接做为new一个Thread的参数,立马执行 。
  2. 当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中。
  3. 有界队列满了之后,如果poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。
  4. 如果3中也无法处理了,就会走到第四步执行reject操作。
  • 无界队列: 与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。

10、synchronized关键字

synchronized简介

  1. synchronized实现同步的基础:Java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁。
  2. synchronzied实现同步的表现形式分为:代码块同步 和 方法同步。

synchronized原理

JVM基于进入和退出Monitor对象来实现 代码块同步 和 方法同步 ,两者实现细节不同。

代码块同步: 在编译后通过将monitorenter指令插入到同步代码块的开始处,将monitorexit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个monitor与之关联,线程执行monitorenter指令时,会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁。

方法同步: synchronized方法在method_info结构有ACC_synchronized标记,线程执行时会识别该标记,获取对应的锁,实现方法同步。

两者虽然实现细节不同,但本质上都是对一个对象的监视器(monitor)的获取任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。

synchronized锁是什么

  • 对于普通同步方法,锁是当前实例对象;
  • 对于静态同步方法,锁是当前类的Class对象;
  • 对于同步方法块,锁是括号中配置的对象;

synchonized(this)和synchonized(object)区别?

其实并没有很大的区别,synchonized(object)本身就包含synchonized(this)这种情况,使用的场景都是对一个代码块进行加锁,效率比直接在方法名上加synchonized高一些,唯一的区别就是对象的不同。

synchronize作用于方法和静态方法区别?

  • 在static方法前加synchronized:静态方法属于类方法,它属于这个类,获取到的锁,是属于类的锁。

  • 在普通方法前加synchronized:非static方法获取到的锁,是属于当前对象的锁。

  • 结论:类锁和对象锁不同,synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

synchronized关键字和Lock的区别?为什么Lock的性能好一些?

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 1、已获取锁的线程执行完同步代码,释放锁;2、线程执行发生异常,jvm会让线程释放锁。 在finally中必须释放锁,不然容易造成线程死锁。
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
性能 少量同步 大量同步

Lock(ReentrantLock)的底层实现主要是Volatile + CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能。但是在JDK1.6以后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制。所以建议一般请求并发量不大的情况下使用synchronized关键字。

11、volatile原理

volatile关键字就是Java中提供的另一种解决可见性有序性问题的方案。对volatile变量的单次读/写操作可保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。volatile也是互斥同步的一种实现,不过它非常的轻量级。

volatile有两条关键的语义:

  • 保证被volatile修饰的变量对所有线程都是可见的

  • 禁止进行指令重排序

如何保证可见性?

被volatile修饰的变量在工作内存修改后会被强制写回主内存,其他线程在使用时也会强制从主内存刷新,这样就保证了一致性。

禁止进行指令重排序

什么是指令重排序?

指令重排序是指指令乱序执行,即在条件允许的情况下直接运行当前有能力立即执行的后续指令,避开为获取一条指令所需数据而造成的等待,通过乱序执行的技术提供执行效率。

synchronized 和 volatile 关键字的作用和区别。

  1. volatile 仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。

  2. volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可以保证变量的修改可见性和原子性。

  3. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

  4. volatile 标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

12、CopyOnWriteArrayList

原理:

CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。

优点和缺点:

优点:

  1. 数据完整,为什么?因为加锁了,并发数据不会乱。

  2. 解决了像ArrayList、Vector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!

缺点:

1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。

2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

13、线程死锁的4个条件?

死锁是如何发生的,如何避免死锁?

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

一般来说,要出现死锁问题需要满足以下条件:

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

死锁程序:

public class DeadLock {
    public static void main(String[] args) {
        // 线程a
        Thread td1 = new Thread(new Runnable() {
            public void run() {
                DeadLock.method1();
            }
        });
        // 线程b
        Thread td2 = new Thread(new Runnable() {
            public void run() {
                DeadLock.method2();
            }
        });

        td1.start();
        td2.start();
    }

    public static void method1() {
        synchronized (String.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程a尝试获取integer.class");
            synchronized (Integer.class) {

            }
        }
    }

    public static void method2() {
        synchronized (Integer.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程b尝试获取String.class");
            synchronized (String.class) {

            }
        }
    }
}

死锁文章:LRH1993写的,感觉还不错。 文章链接

14、怎么安全停止一个线程任务?原理是什么?线程池里有类似机制吗?

终止线程

1、使用violate boolean变量退出标志,使线程正常退出,也就是当run方法完成后线程终止。(推荐)

2、使用interrupt()方法中断线程,但是线程不一定会终止。

3、使用stop方法强行终止线程。不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。

终止线程池

ExecutorService线程池就提供了shutdown和shutdownNow这样的生命周期方法来关闭线程池自身以及它拥有的所有线程。

1、shutdown关闭线程池

线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

2、shutdownNow 关闭线程池并中断任务

终止等待执行的线程,并返回它们的列表。试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt()。shutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

15、生产者与消费者

  • 对于多个生产者和消费者。 为什么要定义while判断标记。 原因:让被唤醒的线程再一次判断标记。
  • 为什么定义notifyAll, 因为需要唤醒对方线程。因为只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。

普通版

调用类:

public class Client {
    public static void main(String[] args) {
        Resource r = new Resource();
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(con);

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

生产者:

public class Producer implements Runnable{
    private Resource res;

    Producer(Resource res) {
        this.res = res;
    }

    @Override
    public void run() {
        while (true) {
            res.set("+商品+");
        }
    }
}

消费者:

public class Consumer implements Runnable {
    private Resource res;

    Consumer(Resource res) {
        this.res = res;
    }

    @Override
    public void run() {
        while (true) {
            res.out();
        }
    }
}

资源类:

public class Resource {
    private String name;
    private int count = 1;
    private boolean flag = false;

    public synchronized void set(String name) {
        while (flag)
            try {
                this.wait();
            } catch (Exception e) {
            }
        this.name = name + "--" + count++;
        System.out.println(Thread.currentThread().getName() + "...生产者.." + this.name);
        flag = true;
        this.notifyAll();
    }

    public synchronized void out() {
        while (!flag)
            try {
                wait();
            } catch (Exception e) {
            }
        System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
        flag = false;
        this.notifyAll();
    }
}

输出:

Thread-1...消费者.........+商品+--495928
Thread-0...生产者..+商品+--495929
Thread-1...消费者.........+商品+--495929
Thread-0...生产者..+商品+--495930
Thread-1...消费者.........+商品+--495930
Thread-0...生产者..+商品+--495931

升级版

  • JDK1.5 中提供了多线程升级解决方案。 将同步Synchronized替换成显示的Lock操作。 将Object中的wait,notify,notifyAll,替换了Condition对象。 该对象可以通过Lock锁进行获取。 该示例中,实现了本方只唤醒对方操作。
  • Lock:替代了Synchronized,lock unlock newCondition()
  • Condition:替代了Object wait notify notifyAll await(); signal(); signalAll();

调用类:

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

        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t1 = new Thread(pro);
        Thread t3 = new Thread(con);

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

生产者和消费者

class Producer2 implements Runnable {
    private Resource res;
    Producer2(Resource res) {
        this.res = res;
    }
    public void run() {
        while (true) {
            res.set("+商品+");
        }
    }
}

class Consumer2 implements Runnable {
    private Resource res;
    Consumer2(Resource res) {
        this.res = res;
    }
    public void run() {
        while (true) {
            res.out();
        }
    }
}

资源类:

class Resource2 {
    private String name;
    private int count = 1;
    private boolean flag = false;
    // t1
    private Lock lock = new ReentrantLock();

    private Condition condition_pro = lock.newCondition();
    private Condition condition_con = lock.newCondition();

    public void set(String name) throws InterruptedException {
        lock.lock();
        try {
            while (flag){
                condition_pro.await();
            }
            this.name = name + "--" + count++;
            System.out.println(Thread.currentThread().getName() + "...生产者.." + this.name);
            flag = true;
            condition_con.signal();
        } finally {
            lock.unlock();// 释放锁的动作一定要执行。
        }
    }

    // t2
    public void out() throws InterruptedException {
        lock.lock();
        try {
            while (!flag){
                condition_con.await();
            }
            System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
            flag = false;
            condition_pro.signal();
        } finally {
            lock.unlock();
        }
    }
}

生产者和消费者的好文章:producer-consumer.md

16、CAS问题

  • CAS原理是什么?

CAS即compare and swap的缩写,中文翻译成比较并交换。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。自旋就是不断尝试CAS操作直到成功为止。

  • CAS实现原子操作会出现什么问题

ABA问题。因为CAS需要在操作之的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成,有变成A,那么使用CAS进行检查时会发现它的值没有发生变化,但实际上发生了变化。ABA问题可以通过添加版本号来解决。

Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。 循环时间长开销大。pause指令优化。 只能保证一个共享变量的原子操作。可以合并成一个对象进行CAS操作。

17、什么是乐观锁和悲观锁

  • 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

  • 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

18、什么是AQS

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

19、Semaphore有什么作用

Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

20、线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

  • Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
  • Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

21、在Java中CycliBarriar和CountdownLatch有什么区别?

CyclicBarrier可以重复使用,而CountdownLatch不能重复使用。

Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。

你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。

所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止

CyclicBarrier一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。