黑马程序员————Java多线程技术详解

                                                     --------  Android培训Java培训、期待与您交流!-------- 


                                                                          一.线程基本知识

1.1 线程的概念

       线程,有时被称为轻量级进程(LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞运行三种基本状态。就绪状态:指线程具备运行的所有条件,逻辑上可以运行,在等待处理机。运行状态:指线程占有处理机正在运行。阻塞状态:指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

1.2 线程状态图












1.3 线程创建方式   

Thread() 
Thread(Runnable target) 
Thread(Runnable target, String name) 
Thread(String name)  指定线程的名字来创建对象
Thread(ThreadGroup group,Runnable target)  指定该线程所属的线程组和Runnable对象来创建线程对象
Thread(ThreadGroup group,Runnable target,String name) 
Thread(ThreadGroup group,Runnable target,String name, long stackSize) stackSize为指定线程所属栈的大小
Thread(ThreadGroup group, String name) 







                                  二.线程同步机制


2.1 synchronized关键字详解  

简介:可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问Object的一个加锁代码块时,另一个线程仍然可以访问该Object中的非加锁代码块。
同步代码块:synchronized(){ }
同步方法:public synchronized static void method(){}。该关键字的使用与对象锁有很大的联系,线程只有取得
象的锁时,才能取得执行权,但前提是线程共享同一个对象琐时才成立;否则线程之间不能实现同步。任意时刻只有一个线程才能取得共享的对象锁,其它的线程将会被阻塞,直到锁拥有者,释放掉锁,其它的线程才有机会取得该对象锁。线程取得锁的顺序是不可预测的,它的实现不是公平锁,阻塞时间长的线程并不会在锁拥有者释放锁时率先取得锁。

可见性:某些虚拟机为了优化,让那些经常需要计算的变量缓存在在自己的缓存中,一遍下次运行的时候,直接在缓存中提取数据,这种做法在单线程中可以提高性能,但在多线程中,会造成程序的不正确性,因为线程之间的缓存是不可见的,一个线程对共享数据的改变,另一个线程是看不到改变的,为此使用该关键字能够让其在一个线程在修改共享变量时,把最新修改的值刷新到缓存,同时通知其它线程在主存中获取值,缓存失效,这样导致每个线程读取的是最新值。
原子性:用该关键字修饰的代码区域,保证了代码区域的原子性,通常不会被中断,除非某些情况的发生,例如调用wait()。
使用场景:对那些临界共享资源使用时才使用同步,否则不要使用同步。
正确的同步方式:对锁的使用一定要小心,不正确使用锁资源会导致程序死锁,切记不要交叉使用锁资源,也即是对synchronized的使用顺序要很小心,下面介绍个死锁的例子:

 public class DeadlockTest {
	public static void main(String[] args) {
	Runnable run1=()->Deadlock.R();
	Runnable run2=()->Deadlock.V();
	Thread t1=new Thread(run1);
	Thread t2=new Thread(run2);
	t1.start();
	t2.start();
	}
}
class Deadlock
{
public static void R()
{
synchronized(Deadlock.class)
{
try {
	Thread.sleep(1000);//用该语句代替程序所要执行的代码
	synchronized(Class.class)
	{
    }
    } catch (InterruptedException e) {
	e.printStackTrace();}}}
public static void V()
{
	synchronized(Class.class)
	{
	try {
		Thread.sleep(1000);//用该语句代替程序所要执行的代码
		synchronized(Deadlock.class)
		{
			
		}} catch (InterruptedException e) {e.printStackTrace();}}}}
利用锁资源和synchronized实现线程互斥打印:

public class DeadlockTest {
	public static void main(String[] args) {
	Runnable run1=()->{try{Deadlock.R();}catch(InterruptedException e){}};
	Runnable run2=()->{try{Deadlock.V();}catch(InterruptedException e){}};
	Thread t1=new Thread(run1);
	Thread t2=new Thread(run2);
	t1.start();
	t2.start();}
}
class Deadlock
{
public static int count =100;
public static boolean flag=false;
public static void R() throws InterruptedException
{
while(count>0)
synchronized(Deadlock.class)
{   
 
    while(!flag) 
	Deadlock.class.wait();
    if(count>0){
    System.out.println(Thread.currentThread().getName()+" : "+count--);
    flag=false;
    Deadlock.class.notify();}
 
}
}

public static void V() throws InterruptedException
{
	while(count>0)
		synchronized(Deadlock.class)
		{   
		 
		  while(flag) 
			Deadlock.class.wait();
		  if(count>0){
	        System.out.println(Thread.currentThread().getName()+" : "+count--);
		    flag=true;
		  Deadlock.class.notify();
		} }
}
}

2.2 volatile关键字详解
volatile写-读的内存语义:
        线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
        线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
        线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。 
JMM内存屏障插入策略:
         在每个volatile写操作的前面插入一个StoreStore屏障。
         在每个volatile写操作的后面插入一个StoreLoad屏障。
         在每个volatile读操作的后面插入一个LoadLoad屏障。
         在每个volatile读操作的后面插入一个LoadStore屏障。 
happens-before准则:
         同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。
         对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。
         对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。
         Thread.start()的调用会happens-before于启动线程里面的动作。
         Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join() 中返回或者Thread.isAlive()==false。
         一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())

                                                                           三.锁机制

 
3.1 ReentrantLock
简介:
一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。ReentrantLock将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用lock的线程将成功获取该锁并返回此类的构造方法接受一个可选的公平参数。当设置为true时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的tryLock方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。
3.2 Condition
简介:
Condition将Object监视器方法分解成截然不同的对象,以便通过将这些对象与任意Lock 实现组合使用,为每个对象提供多个等待set(wait-set)。其中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。条件(也称为条件队列或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为true的另一个线程通知它之前,一直挂起该线程(即让其“等待”)因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式释放相关的锁,并挂起当前线程,就像Object.wait做的那样。Condition实例实质上被绑定到一个锁上。要为特定Lock实例获得Condition实例,请使用方法。
作为一个示例,假定有一个绑定的缓冲区,它支持put和take方法。如果试图在空的缓冲区上执行take操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行put操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待set中保存put线程和 take线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition实例来做到这一点。

 classBoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition();
   final Condition notEmpty =lock.newCondition();
   final Object[] items = new Object[100];
   int putptr, takeptr, count;
   public void put(Object x) throwsInterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr =0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }
   public Object take() throwsInterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr =0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }
利用Lock和Condition实现交叉打印
publicclass LockTest {
        public static void main(String[] args)throws  Exception {
         Runnable run1=()->{try {
                for(int i=10;i>0;i--)
               Count.printC();
        } catch (Exception e) {
               // TODO Auto-generated catchblock
               e.printStackTrace();
        }};
         Runnable run2=()->{try {
                for(int i=10;i>0;i--)
               Count.printM();
        } catch (Exception e) {
               // TODO Auto-generated catchblock
               e.printStackTrace();
        }};
         Thread t1=new Thread(run1);
         Thread t2=new Thread(run2);
         t1.start();
 
         t2.start();
        }
 
}
classCount
{
privatestatic ReentrantLock lock=new ReentrantLock();
privatestatic Condition tag= lock.newCondition();
privatestatic Condition fag= lock.newCondition();
privatestatic boolean flag=false;
publicstatic void printC() throws InterruptedException
{
lock.lock();
while(!flag)  tag.await();
System.out.println("中国");
flag=false;
tag.signal(); 
lock.unlock();
}
publicstatic void printM() throws InterruptedException
{
lock.lock();
while(flag)  tag.await();
System.out.println("美国");
flag=true;
tag.signal(); 
lock.unlock();
}
}
3.3 ReentrantReadWriteLock
获取顺序:此类不会将读取者优先或写入者优先强加给锁访问的排序。但是,它确实支持可选的公平策略。
非公平模式(默认):当非公平地(默认)构造时,未指定进入读写锁的顺序,受到reentrancy约束的限制。连续竞争的非公平锁可能无限期地推迟一个或多个reader或writer线程,但吞吐量通常要高于公平锁。
公平模式:当公平地构造线程时,线程利用一个近似到达顺序的策略来争夺进入。当释放当前保持的锁时,可以为等待时间最长的单个writer线程分配写入锁,如果有一组等待时间大于所有正在等待的writer线程的reader线程,将为该组分配写入锁。如果保持写入锁,或者有一个等待的writer线程,则试图获得公平读取锁(非重入地)的线程将会阻塞。直到当前最旧的等待 writer线程已获得并释放了写入锁之后,该线程才会获得读取锁。当然,如果等待writer放弃其等待,而保留一个或更多reader线程为队列中带有写入锁自由的时间最长的waiter,则将为那些 reader分配读取锁。试图获得公平写入锁的(非重入地)的线程将会阻塞,除非读取锁和写入锁都自由(这意味着没有等待线程)(注意,非阻塞ReentrantReadWriteLock.ReadLock.tryLock()和ReentrantReadWriteLock.WriteLock.tryLock()方法不会遵守此公平设置,并将获得锁(如果可能),不考虑等待线程)。
重入:此锁允许reader和writer按照ReentrantLock的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入reader使用它们。此外,writer可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果reader试图获取写入锁,那么将永远不会获得成功。
锁降级:重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。
锁获取的中断:读取锁和写入锁都支持锁获取期间的中断。
Condition支持:写入锁提供了一个Condition实现,对于写入锁来说,该实现的行为与ReentrantLock.newCondition()提供的Condition实现对ReentrantLock所做的行为相同。当然,此Condition只能用于写入锁。读取锁不支持 Condition,readLock().newCondition()会抛出UnsupportedOperationException。
读写锁实例:

publicclass CachedData {
   static ReentrantReadWriteLock rwl = newReentrantReadWriteLock();
   static Lock readLock=rwl.readLock();
   static Map<String, Long> map=newHashMap<>();
   public static  void processCachedData() {
          readLock.lock();
     if (cacheValid())
     {
      rwl.readLock().unlock();
      rwl.writeLock().lock();
      if(cacheValid())
      {
      map.put("中国", (long) (Math.random()*379999+19111010));
      }
      rwl.readLock().lock();
      rwl.writeLock().unlock();
     }
     System.out.println(map.get("中国"));
     map.remove("中国");
     rwl.readLock().unlock();
 
 }
   public static boolean cacheValid()
   {
         
                 return map.isEmpty();                  
          
   }}
读写锁实例:

public class ReadWriteLockTest {
	public static void main(String[] args) {
		final Queue3 q3 = new Queue3();
		for(int i=0;i<3;i++)
		{
			new Thread(){
				public void run(){
					while(true){
						q3.get();						
					}
				}
				
			}.start();

			new Thread(){
				public void run(){
					while(true){
						q3.put(new Random().nextInt(10000));
					}
				}			
				
			}.start();
		}
		
	}
}

class Queue3{
	private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
	ReadWriteLock rwl = new ReentrantReadWriteLock();
	public void get(){
		rwl.readLock().lock();
		try {
			System.out.println(Thread.currentThread().getName() + " be ready to read data!");
			Thread.sleep((long)(Math.random()*1000));
			System.out.println(Thread.currentThread().getName() + "have read data :" + data);			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.readLock().unlock();
		}
	}
	
	public void put(Object data){

		rwl.writeLock().lock();
		try {
			System.out.println(Thread.currentThread().getName() + " be ready to write data!");					
			Thread.sleep((long)(Math.random()*1000));
			this.data = data;		
			System.out.println(Thread.currentThread().getName() + " have write data: " + data);					
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.writeLock().unlock();
		}
		
	
	}
}

                               四.信号资源

public class SemaphoreTest {
	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		final  Semaphore sp = new Semaphore(3);
		for(int i=0;i<10;i++){
			Runnable runnable = new Runnable(){
					public void run(){
					try {
						sp.acquire();
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
					System.out.println("线程" + Thread.currentThread().getName() + 
							"进入,当前已有" + (3-sp.availablePermits()) + "个并发");
					try {
						Thread.sleep((long)(Math.random()*10000));
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("线程" + Thread.currentThread().getName() + 
							"即将离开");					
					sp.release();
					//下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
					System.out.println("线程" + Thread.currentThread().getName() + 
							"已离开,当前已有" + (3-sp.availablePermits()) + "个并发");					
				}
			};
			service.execute(runnable);			
		}
	}

}

                                     五.线程池

线程池的创建方式:

static ExecutorService newCachedThreadPool() 
          创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。 
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) 
          创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提           供的 ThreadFactory 创建新线程。 
static ExecutorService newFixedThreadPool(int nThreads) 
          创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。 
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) 
          创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在需要时使用提供的               ThreadFactory 创建新线程。 
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 
          创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) 
          创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 
static ExecutorService newSingleThreadExecutor() 
          创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。 
static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) 
          创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程,并在需要时使用提供的 ThreadFactory 创建新线程。 
static ScheduledExecutorService newSingleThreadScheduledExecutor() 
          创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。 
static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) 
          创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。 

public class ThreadPoolTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		//ExecutorService threadPool = Executors.newFixedThreadPool(3);
		//ExecutorService threadPool = Executors.newCachedThreadPool();
		ExecutorService threadPool = Executors.newSingleThreadExecutor();
		for(int i=1;i<=10;i++){
			final int task = i;
			threadPool.execute(new Runnable(){
				@Override
				public void run() {
					for(int j=1;j<=10;j++){
						try {
							Thread.sleep(20);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName() + " is looping of " + j + " for  task of " + task);
					}
				}
			});
		}
		System.out.println("all of 10 tasks have committed! ");
		//threadPool.shutdownNow();
		
		Executors.newScheduledThreadPool(3).scheduleAtFixedRate(
				new Runnable(){
					@Override
				public void run() {
					System.out.println("bombing!");
					
				}},
				6,
				2,
				TimeUnit.SECONDS);
	}

}

                                   六.ThreadLocal

简介:该类提供了线程局部(thread-local)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static字段,它们希望将状态与某一个线程(例如,用户ID或事务ID)相关联。

public class ThreadLocalTest {

	private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
	private static ThreadLocal<MyThreadScopeData> myThreadScopeData = new ThreadLocal<MyThreadScopeData>();
	public static void main(String[] args) {
		for(int i=0;i<2;i++){
			new Thread(new Runnable(){
				@Override
				public void run() {
					int data = new Random().nextInt();
					System.out.println(Thread.currentThread().getName() 
							+ " has put data :" + data);
					x.set(data);
/*					MyThreadScopeData myData = new MyThreadScopeData();
					myData.setName("name" + data);
					myData.setAge(data);
					myThreadScopeData.set(myData);*/
					MyThreadScopeData.getThreadInstance().setName("name" + data);
					MyThreadScopeData.getThreadInstance().setAge(data);
					new A().get();
					new B().get();
				}
			}).start();
		}
	}
	
	static class A{
		public void get(){
			int data = x.get();
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " get data :" + data);
/*			MyThreadScopeData myData = myThreadScopeData.get();;
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + "," +
					myData.getAge());*/
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + "," +
					myData.getAge());
		}
	}
	
	static class B{
		public void get(){
			int data = x.get();			
			System.out.println("B from " + Thread.currentThread().getName() 
					+ " get data :" + data);
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("B from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + "," +
					myData.getAge());			
		}		
	}
}

class MyThreadScopeData{
	private MyThreadScopeData(){}
	public static /*synchronized*/ MyThreadScopeData getThreadInstance(){
		MyThreadScopeData instance = map.get();
		if(instance == null){
			instance = new MyThreadScopeData();
			map.set(instance);
		}
		return instance;
	}
	//private static MyThreadScopeData instance = null;//new MyThreadScopeData();
	private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();
	
	private String name;
	private int age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

                           七.多线程经典例子 

package thread;
/*
 * 哲学家就餐问题
 */
 /*每个哲学家相当于一个线程*/
class Philosopher extends Thread{
    private String name;
    private Fork fork;
    public Philosopher(String name,Fork fork){
        super(name);
        this.name=name;
        this.fork=fork;
    }

    public void run(){
        while(true){
            thinking();
            fork.takeFork();
            eating();
            fork.putFork();
        }

    }

    
    public void eating(){
        System.out.println("I am Eating:"+name);
        try {
            sleep(1000);//模拟吃饭,占用一段时间资源
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    
    public void thinking(){
        System.out.println("I am Thinking:"+name);
        try {
            sleep(1000);//模拟思考
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

class Fork{
    /*5只筷子,初始为都未被用*/
    private boolean[] used={false,false,false,false,false,false};

    /*只有当左右手的筷子都未被使用时,才允许获取筷子,且必须同时获取左右手筷子*/
    public synchronized void takeFork(){
        String name = Thread.currentThread().getName();
        int i = Integer.parseInt(name);
        while(used[i]||used[(i+1)%5]){
            try {
                wait();//如果左右手有一只正被使用,等待
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        used[i ]= true;
        used[(i+1)%5]=true;
    }

    /*必须同时释放左右手的筷子*/
    public synchronized void putFork(){
        String name = Thread.currentThread().getName();
        int i = Integer.parseInt(name);

        used[i ]= false;
        used[(i+1)%5]=false;
        notifyAll();//唤醒其他线程
    }
}

//测试
public class ThreadTest {

    public static void main(String []args){
        Fork fork = new Fork();
        new Philosopher("0",fork).start();
        new Philosopher("1",fork).start();
        new Philosopher("2",fork).start();
        new Philosopher("3",fork).start();
        new Philosopher("4",fork).start();
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值