多线程编程以及线程池相关记录

Java 中经常需要用到多线程来处理一些业务,非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。

java中涉及到线程池的相关类均在 jdk 1.5 开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable

1.多线程编程时如何设置线程池的大小?

计算密集型

顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:

线程数 = CPU核数+1,也可以设置成CPU核数*2,但还要看JDK的版本以及CPU配置(服务器的CPU有超线程)。一般设置CPU * 2即可。

O密集型

我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。

因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。那么这个线程池的数据量是不是可以随便设置呢?当然不是的,请一定要记得,线程上下文切换是有代价的。目前总结了一套公式,对于IO密集型应用:

线程数 = CPU核心数/(1-阻塞系数) 这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。

套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整:final int poolSize = (int)(cpuCore/(1-0.9))

2.创建线程的三种方式

 a.通过继承Thread类创建线程类。

 b.实现Runnable接口创建线程类。Thread也是实现了Runnable接口

 c.通过 Callable 和 Future 接口创建线程

Callable和Future,一个产生结果,一个拿到结果。 Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。

Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法call(),这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable要采用ExecutorSevice的submit方法提交,返回值是Future对象,可以通过这个获取线程返回结果。要执行Callable实例,需要先生成ExecutorService实例,在ExecutorService接口中有若干个submit方法的重载版本,一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。

Future是一个接口,它可以对Callable任务的执行结果进行操作。可以说Future提供了三种功能:判断任务是否完成;能够中断任务;能够获取任务执行结果

boolean cancel(boolean mayInterruptIfRunning);

boolean isCancelled();

boolean isDone();

V get() throws InterruptedException, ExecutionException;

V get() throws InterruptedException, ExecutionException;

 cancel()方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。

isCancelled()方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。

isDone()方法表示任务是否已经完成,若任务完成,则返回true;

get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接

Callable的使用案例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class AtomicIntegerFieldUpdaterTest {
 
	public static <T> void main(String[] args) {
 
		ExecutorService newFixedThreadPool = Executors.newSingleThreadExecutor();
		Future<String> submit = newFixedThreadPool.submit(new Callable<String>() {
			@Override
			public String call() throws Exception {
				// TODO Auto-generated method stub
				return "我是生产的结果";
			}
		});
		try {
			System.out.println("我来拿结果了:"+submit.get());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
	}
 
}

线程的生命周期,它们之间如何切换 

线程的生命周期包含 5 个阶段,包括:新建、就绪、运行、阻塞、销毁。 

 新建(NEW):就是刚使用 new 方法,new 出来的线程;

 就绪(RUNNABLE):就是调用的线程的 start()方法后,这时候线程处于等待 CPU 分 
 配资源阶段,谁先抢的 CPU 资源,谁开始执行; 

 运行(RUNNING):当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run 
 方法定义了线程的操作和功能; 

阻塞(BLOCKED):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了 阻塞状态,比如 sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将 处于阻塞状态的线程唤醒,比如调用 notify 或者 notifyAll()方法。唤醒的线程不会立刻 执行 run 方法,它们要再次等待 CPU 分配资源进入运行状态; Waiting(无限等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进 入 Waiting 状态。进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 方法 或者 notifyAll 方法时才能够被唤醒。 

销毁(TERMINATED):如果线程正常执行完毕后或线程被提前强制性的终止或出现异 
常导致结束,那么线程就要被销毁,释放资源;

多进程开发中 join 和 deamon 的区别 
join:当子线程调用 join 时,主线程会被阻塞,当子线程结束后,主线程才能继续执行。 
deamon:当子进程被设置为守护进程时,主进程结束,不管子进程是否执行完毕,都会随着 
主进程的结束而结束

3.创建线程池的几种方式以及线程池的参数

Java 创建线程池主要有两种方法,一种是通过 Executors 工厂类提供的方法,该类提供了4种不同的线程池;另一种是通过 ThreadPoolExecutor类进行自定义创建。

Executors静态方法创建线程池:

a)   newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵 
      活回收空闲线程,若无可回收,则新建线程。 
b)  newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队 
     列中等待。 
c) newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 

d) newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执 
  行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ThreadPoolExecutor类提供了4种构造方法,可根据需要来自定义一个线程池

 虽然定义四个构造放发创建线程池,但是在构造方法里面都是调用第四个构造方法。第四个构造方法中有7个核心参数。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

  (1)corePoolSize:核心线程数,线程池中始终存活的线程数。

(2)maximumPoolSize: 最大线程数,线程池中允许的最大线程数。

(3)keepAliveTime: 存活时间,线程没有任务执行时最多保持多久时间会终止。

(4)unit: 单位,参数 keepAliveTime 的时间单位,7种可选。

参数        描述
TimeUnit.DAYS
TimeUnit.HOURS小时
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS毫秒
TimeUnit.MICROSECONDS微妙
TimeUnit.NANOSECONDS纳秒

(5)workQueue: 一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。

参数描述
ArrayBlockingQueue一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue一个由链表结构组成的有界阻塞队列。
SynchronousQueue一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列。
DelayQueue一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque一个由链表结构组成的双向阻塞队列。

较常用的是 LinkedBlockingQueue 和 SynchronousQueue。线程池的排队策略与 BlockingQueue 有关。

(6)threadFactory: 线程工厂,主要用来创建线程,默认正常优先级、非守护线程。

(7)handler:拒绝策略,拒绝处理任务时的策略,4种可选,默认为 AbortPolicy

参数描述
AbortPolicy拒绝并抛出异常。
CallerRunsPolicy重试提交当前的任务,即再次调用运行该任务的execute()方法。
DiscardOldestPolicy抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy抛弃当前任务。

自定义拒绝策略可以实现RejectedExecutionHandler接口。

线程池的执行规则如下:

(1)当线程数小于核心线程数时,创建线程。

(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

(3)当线程数大于等于核心线程数,且任务队列已满:

若线程数小于最大线程数,创建线程。

若线程数等于最大线程数,抛出异常,拒绝任务

package www.itbac.com;
import java.util.concurrent.*;
public class ExecutorTest {
    
    public static void main(String[] args)   {
        // 创建线程池 , 参数含义 :(核心线程数,最大线程数,加开线程的存活时间,时间单位,任务队列长度)
        ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 8,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(2));
        
        //设置:任务数 = 3 ~ 11 ,分析:任务数 与 活跃线程数,核心线程数,队列长度,最大线程数的关系。

        int a = 3;
    
            for (int i = 1; i <= a; i++) {
                int j = i;
                pool.submit(new Runnable() {
                    @Override
                    public void run() {
                        //获取线程名称
                        Thread thread = Thread.currentThread();
                        String name = thread.getName();
                        //输出
                        int activeCount = pool.getActiveCount();
                        System.out.println("任务:"+j+"-----,线程名称:"+name+"-----活跃线程数:"+activeCount);
                    }
                });
            }
            
        //关闭线程池
        pool.shutdown();

    }
}

输出结果,观察关系:

//任务数 a = 3 , 活跃线程数3 , 任务数 < 核心线程数。
//任务数 a = 4 , 活跃线程数4 , 任务数 < 核心线程数。
//任务数 a = 5 , 活跃线程数5 , 任务数 = 核心线程数。
//任务数 a = 6 , 活跃线程数5 , 任务数 < 核心线程数5 + 队列长度2 。
//任务数 a = 7 , 活跃线程数5 , 任务数 = 核心线程数5 + 队列长度2 。

//任务数 a = 8 , 活跃线程数6 , 任务数 < 最大线程数8 + 队列长度2 。活跃线程数是在核心线程数5的基础上,加1个活跃线程。
//任务数 a = 9 , 活跃线程数7 , 任务数 < 最大线程数8 + 队列长度2 。活跃线程数是在核心线程数5的基础上,加2个活跃线程。
//任务数 a = 10 , 活跃线程数8 , 任务数 = 最大线程数8 + 队列长度2 。活跃线程数是在核心线程数5的基础上,加3个活跃线程。

//任务数 a = 11 , 活跃线程数8 , 任务数 > 最大线程数8 + 队列长度2 。抛出异常RejectedExecutionException

总结:
随着任务数量的增加,会增加活跃的线程数。

当活跃的线程数 = 核心线程数,此时不再增加活跃线程数,而是往任务队列里堆积。

当任务队列堆满了,随着任务数量的增加,会在核心线程数的基础上加开线程。

直到活跃线程数 = 最大线程数,就不能增加线程了。

如果此时任务还在增加,则: 任务数11 > 最大线程数8 + 队列长度2 ,抛出异常RejectedExecutionException,拒绝任务。

注意:当核心线程数为0,开始也会创建一个活跃 线程,然后再有任务后则往队列里面放,队列放满了后增加活跃线程,直到增加到 活跃线程数 = 最大线程数。所以线程池最大能容入的线程是最大线程数 + 队列大小。

execute和submit的区别与联系
execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。

execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

execute()方法的返回类型是 void,它定义在 Executor 接口中。而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口.

虽然看上去 Executors 类的封装,可以简化我们的使用,但事实上,不建议使用 Executors 类提供的这4种方法:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors返回的线程池对象的弊端如下:

FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

4.实现多线程同步有方法 

 a. 使用 synchronized 关键字和wait 和 notify 结合实现同步。

 b.使用特殊域变量 volatile 实现线程同步

 c.使用重入锁实现线程同步

可重入锁和不可重入锁理解(转载 对可重入锁和不可重入锁的理解,他们的区别及实现原理解析。_Anakki的博客-CSDN博客_可重入锁和不可重入锁的区别):首先我们这里提到的锁,是把所需要的代码块,资源,或数据锁上,在操作他们的时候只允许一个线程去做操作。最终结果是为了保证cpu计算结果的正确性。对不可重入锁的理解:

public class Test{
     Lock lock = new Lock();
     public void methodA(){
         lock.lock();
         ...........;
         methodB();
         ...........;
         lock.unlock();
     }
     public void methodB(){
         lock.lock();
         ...........;
         lock.unlock();
     }
}

当A方法获取lock锁去锁住一段需要做原子性操作的B方法时,如果这段B方法又需要锁去做原子性操作,那么A方法就必定要与B方法出现死锁。这种会出现问题的重入一把锁的情况,叫不可重入锁。A方法需要等B方法执行完才能解锁,但是B方法想执行完代码又必须要lock锁来加锁。A的锁未解锁前,其他代码块无法使用此锁来加锁。这是由这个不可重入锁决定的。

不可重入锁:

public class Lock{
     private boolean isLocked = false;
     public synchronized void lock() throws InterruptedException{
         while(isLocked){    
             wait();
         }
         isLocked = true;
     }
     public synchronized void unlock(){
         isLocked = false;
         notify();
    }
}

那么平时我们又有需要重入一把锁的需求!!!!比如A方法是个原子性操作,但它有需要调用B方法的原子性操作,他们还争抢的是同一个临界资源,因此需要同一把锁来加锁(ps:争抢同一临界资源的实质就是对同一把锁的争抢)针对此情况,就有了可重入锁的概念:可重入锁的实现:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

可以看见代码的核心概念是:首先解释lockedBy:顾名思义,临界资源被哪个线程锁住了。加锁时,先获取当前线程。(识别谁需要锁)

 Thread thread = Thread.currentThread();

判断:当临界资源已被锁上,但当前请求锁的线程又不是之前锁上临界资源的线程。那么当前请求锁的线程需要等待。

while(isLocked && lockedBy != thread){
        wait();
}

注意上面是个while,并且是个wait,因此当请求线程请求不到锁的时候,就wait了。当时当while不满足有的3种情况:

A:当前锁没有线程使用.

B:当前锁有线程使用,当前请求锁的线程就是现在正在使用锁的线程。

C:当前锁没有线程使用,当前请求锁的线程就是现在正在使用锁的线程。(不可能出现,锁0没有被用,哪还有线程使用锁) 来看看

A:没有线程使用:那么:

 isLocked = true;
 lockedCount++;
 lockedBy = thread;

当前请求锁的线程先把锁加上,然后把上锁次数+1,然后把自己(本线程)赋值给lockedBy,以说明当前谁用了这把锁方便之后重入的时候做while判断。

再来看解锁:

 public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }

首先看看要求解锁的线程是不是当前用锁的线程。不是则什么也不做。(当然不能随意让其他的线程一执行unlock代码就能解锁使用啊。那这样相当于谁都有一把钥匙了,这里这个判断也就是说明解锁的必须是加锁的)

如果要求解锁的就是加锁的线程。那么把加锁次数减一。然后在判断加锁次数有没有变为0。

变为0说明,这个锁已经完全解锁了。锁上标识islocked可以复位了。并且随机唤醒某个被wait()等待的线程  :   notify()这就是重入锁的设计。

它和不可重入锁的设计不同之处:

不可重入锁:只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单

可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。

设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。不然没有加锁的参考值,也就不知道什么时候解锁?解锁多少次?才能保证本线程已经访问完临界资源了可以唤醒其他线程访问了。实现相对复杂。

总结:这个重入的概念就是,拿到锁的代码能不能多次以不同的方式访问临界资源而不出现死锁等相关问题。经典之处在于判断了需要使用锁的线程是否为加锁的线程。如果是,则拥有重(chong)入的能力。

 Synchronized、ReentrantLock 的区别 

synchronized 是关键字,ReentrantLock 是 API 接口

Lock 需要手动加锁,手动释放锁

synchronized 不可中断,ReentrantLock 可中断、可超时

synchronized 是非公平锁,ReentrantLock 公平、非公平皆可

ReentrantLock 支持 Condition,多条件

5.ThreadLocal类的使用和理解

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

  ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

set方法源码

 public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//放入Map中
        else
            createMap(t, value);
    }

Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null

,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。

应用场景:

JDBC 连接

Session 管理

Spring 事务管理

调用链,参数传递

AOP

由于 JDBC 的连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时, 就不是线程安全的。通过将 JDBC 的连接对象保存到 ThreadLocal 中,每 个线程都会拥有属于自己的连接对象副本。

6、线程 & 进程的区别

操作系统中可以拥有多个进程,一个进程里可以拥有多个线程,线程在进程内执行进程和线程的区别

l 容易创建新线程。

创建新进程需要重复父进程线程可以控制同一进程的其他线程。进程无法控制兄弟进程,只能控制其子进程进程拥有自己的内存空间。线程使用进程的内存空间,且要和该进程的其他线程共享这个空间;而不是在进程中给每个线程单独划分一点空间。(同一进程中的)线程在共享内存空间中运行,而进程在不同的内存空间中运行线程可以使用 wait(),notify(),notifyAll()等方法直接与其他线程(同一进程)通信;而,进程需要使用“进程间通信”(IPC)来与操作系统中的其他进程通信。

7、Java 进程间的几种通信方式

l 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

8、乐观锁和悲观锁的理解及如何实现,有哪些实现方式

悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会 上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多 这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,

其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式: 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变 量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 ——需要读写的内存位置(V)、进行比较的预期原值 (A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

CAS 缺点: ABA 问题: 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A, 并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而 浪费更多的 CPU 资源,效率低于 synchronized。只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个 共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

9.在 java 中守护线程和本地线程区别

java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolon);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在 Thread.start()之前调用,否则运行时会抛出异常。两者的区别:

唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离, 不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。

扩展:Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程。

10 .什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomicclasses)

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不 一致必须的手段。 int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。 java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器: AtomicLongFieldUpdaterAtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean 来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)

如何停止一个正在运行的线程

使用共享变量的方式在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。使用 interrupt 方法终止线程如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用 Thread.join()方法,或者Thread.sleep()方法,在网络中调用 ServerSocket.accept()方法,或者调用了 DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态 时,即使主程序中 将该线程的共享变量设置为 true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用 stop()方法,而是使用 Thread 提供的 interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一 个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值