Java多线程学习总结

时间:2022-01-27 18:29:23

一、相关概念

    进程:每个进程都有独立的代码和数据空间,进程间的切换回有较大的开销,一个进程包含1—n个线程。(进程是资源分配的最小单位)

    线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,切换开销小。(线程是CPU调度的最小单位)

    线程三大特性:可见性(Volatile保证可见但不保证原子性)、原子性(同步)、有序性。

    在java中,每次程序运行至少启动2个线程,一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际就是在操作系统中启动了一个线程。

    随机性原理:由CPU的快速切换造成,哪个线程获取到了CPU的执行权,哪个线程就执行。

    返回当前线程的名称:Thread.currentThread().getName();

    线程的名称:Thread-编号(编号从0开始)

    线程要运行的代码统一存放在了run方法中。

二、实现的三种方式

    (一)继承Thread类

        Thread类本质上是实现了Runnable接口的一个实例,它代表一个线程的实例。并且,启动线程的唯一方法就是通过Thread类的start()方法,这个方法是一个native方法,它将启动一个新线程,并执行run()方法。

public class ThreadDemo extends Thread {
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName()+" "+i);
		}		
	}
	public static void main(String[] args) {
		ThreadDemo t1=new ThreadDemo();
		ThreadDemo t2=new ThreadDemo();
		t1.start();
		t2.start();
	}
}

        注意:通过Thread类的方式,可以完成多线程的建立,但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承thread类,因为java单继承的局限性。

    (二)实现Runnable接口

       Runnable接口将线程要执行的任务封装成了对象, 实现Runnable接口可以避免单继承的局限性。

public class ThreadDemo implements Runnable {
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName()+" "+i);
		}		
	}
	public static void main(String[] args) {
		Thread t1=new Thread(new ThreadDemo());
		Thread t2=new Thread(new ThreadDemo());
		t1.start();
		t2.start();
	}
}
    (三)实现Callable接口,并与Future、线程池结合使用

        前两种方式线程执行完后都没有返回值,只有这种是带返回值的。无返回值得任务必须实现Runnable接口,类似的,可返回值的任务必须实现Callable接口。执行Callable任务后,可以获取一个Future对象,在该对象上调用get就可已获取到Callable任务返回的Object了,再结合线程池接口就可以实现返回有结果的多线程。

@SuppressWarnings("unchecked")
public class Test4 {
	@SuppressWarnings("rawtypes")
	public static void main(String[] args) throws Exception, ExecutionException {
		System.out.println("——程序开始运行——");
		Date date1 = new Date();
		int taskSize = 5;
		ExecutorService executorService = Executors.newFixedThreadPool(taskSize);
		List<Future> list = new ArrayList<Future>();
		for (int i = 0; i < taskSize; i++) {
			Callable c = new MyCallable(i + " ");
			Future f = executorService.submit(c);
			list.add(f);
		}
		executorService.shutdown();
		for (Future f : list) {
			System.out.println(">>>" + f.get().toString());
		}
		Date date2 = new Date();
		System.out.println("——程序结束运行——,程序运行时间【" + (date2.getTime() - date1.getTime() + "毫秒】"));
	}
}


class MyCallable implements Callable<Object> {
	private String taskNum;


	MyCallable(String taskNum) {
		this.taskNum = taskNum;
	}


	@Override
	public Object call() throws Exception {
		System.out.println(">>>" + taskNum + "任务启动");
		Date d1 = new Date();
		Thread.sleep(1000);
		Date d2 = new Date();
		long time = d2.getTime() - d1.getTime();
		System.out.println(">>>" + taskNum + "任务终止");
		return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
	}
}

运行结果为:

——程序开始运行——
>>>0 任务启动
>>>1 任务启动
>>>2 任务启动
>>>4 任务启动
>>>3 任务启动
>>>0 任务终止
>>>0 任务返回运行结果,当前任务时间【1000毫秒】
>>>2 任务终止
>>>1 任务终止
>>>1 任务返回运行结果,当前任务时间【1000毫秒】
>>>2 任务返回运行结果,当前任务时间【1000毫秒】
>>>4 任务终止
>>>3 任务终止
>>>3 任务返回运行结果,当前任务时间【1000毫秒】
>>>4 任务返回运行结果,当前任务时间【1000毫秒】
——程序结束运行——,程序运行时间【1056毫秒】

三、线程生命周期

    线程生命周期图如下:

Java多线程学习总结

        1、新建状态(New):新创建了一个线程对象。
        2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
        3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
        4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
               (一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
               (二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
              (三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
         5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

四、线程调度及常用方法

    (一)线程优先级

        1、java优先级用整数表示,取值范围1~10,thread类有以下三个静态常量:

                 1)static int MAX_PRIORITY  //最高优先级,取值为10

                 2)static int MIN_PRIORITY  //最低优先级,取值为1

                 3)static int NORM_PRIORITY  //默认优先级,取值为5

         2、Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

           3、每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。

线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级

    (二)常用方法
     isAlive(): 判断一个线程是否存活。 
     join(): 等待线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。 
     activeCount(): 程序中活跃的线程数。 
     enumerate(): 枚举程序中的线程。 
          currentThread(): 得到当前线程。 
     isDaemon(): 一个线程是否为守护线程。 
     setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
     setName(): 为线程设置一个名称。 
     wait(): 强迫一个线程等待。 
     notify(): 唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。 
     setPriority(): 设置一个线程的优先级。
          sleep(): 强迫一个线程睡眠N毫秒。 
          yield():暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

          interrupt():不要以为它是中断某个线程,它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的。

public class Test2 {
public static void main(String[] args) {
Thread t1=new Thread(new Aaa());
Thread t2=new Thread(new Aaa());
t1.setName("1:");
t1.setPriority(Thread.MAX_PRIORITY);
System.out.println(t1.getPriority());
//t2.setPriority(1);
t1.start();
try {
//t1.join();
t1.join();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
t2.start();
System.out.println("开始休眠---");
try {

Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("休眠后---");
}
}
class Aaa implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}	
}

          注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。

五、线程同步

    (一)多线程安全

             原因:当多个线程共享同一个全局变量,做写的时候,可能会受到其他线程的干扰,导致数据有问题。(做读的时候,不会产生线程安全问题)

                解决:同步。(保证数据原子性)

     (二)线程同步方式(5种)

              1、同步方法

                   即用synchronized关键字修饰的方法。由于java的每个对象都有一个this锁,当用此关键字修饰方法时,this锁会保护整个方法。在调用该方法前,需要获得this锁,否则就处于阻塞状态。

public synchronized void save(){
}
            注意:synchronized也可以修饰static,对象是 类名.class,锁住的是整个类。因为static随类的加载而加载,并且优于对象存在,所以可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经封装成了对象,这个对象就是字节码文件对象。所以静态加载时,只有一个对象存在,静态同步方法使用的就是这个对象。

            2、同步代码块

                 同步是一种高开销的操作,因此应该尽量减少同步的内容。 

                 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 
/**
 * 
 * 
 * Title: 多线程模拟购票窗口
 * 
 * Description: 熟练同步代码块和同步函数的使用
 * 
 * @author Marmara
 * 
 * @date 2018年5月11日
 */
public class TicketSale implements Runnable {

	private int tc = 100;

	// private Object o1=new Object();
	public static void main(String[] args) {
		TicketSale t = new TicketSale();
		Thread t1 = new Thread(t, "一号窗口");
		Thread t2 = new Thread(t, "二号窗口");

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

	}

	@Override
	public void run() {

		while (tc > 0) {
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			sale();
		}
	}

	public synchronized void sale() {

		if (tc > 0) {
			System.out.println(Thread.currentThread().getName() + "正在出售第:" + (100 - tc + 1) + "张票。");
			tc--;
		}
	}

}

运行结果:

二号窗口正在出售第:1张票。
一号窗口正在出售第:2张票。
一号窗口正在出售第:3张票。
二号窗口正在出售第:4张票。
一号窗口正在出售第:5张票。
二号窗口正在出售第:6张票。
一号窗口正在出售第:7张票。
二号窗口正在出售第:8张票。
一号窗口正在出售第:9张票。
二号窗口正在出售第:10张票。
二号窗口正在出售第:11张票。
一号窗口正在出售第:12张票。
一号窗口正在出售第:13张票。
二号窗口正在出售第:14张票。
二号窗口正在出售第:15张票。
一号窗口正在出售第:16张票。
二号窗口正在出售第:17张票。
一号窗口正在出售第:18张票。
二号窗口正在出售第:19张票。
一号窗口正在出售第:20张票。
二号窗口正在出售第:21张票。
一号窗口正在出售第:22张票。
二号窗口正在出售第:23张票。
一号窗口正在出售第:24张票。
二号窗口正在出售第:25张票。
一号窗口正在出售第:26张票。
一号窗口正在出售第:27张票。
二号窗口正在出售第:28张票。
二号窗口正在出售第:29张票。
一号窗口正在出售第:30张票。
一号窗口正在出售第:31张票。
二号窗口正在出售第:32张票。
二号窗口正在出售第:33张票。
一号窗口正在出售第:34张票。
一号窗口正在出售第:35张票。
二号窗口正在出售第:36张票。
二号窗口正在出售第:37张票。
一号窗口正在出售第:38张票。
一号窗口正在出售第:39张票。
二号窗口正在出售第:40张票。
一号窗口正在出售第:41张票。
二号窗口正在出售第:42张票。
二号窗口正在出售第:43张票。
一号窗口正在出售第:44张票。
二号窗口正在出售第:45张票。
一号窗口正在出售第:46张票。
一号窗口正在出售第:47张票。
二号窗口正在出售第:48张票。
一号窗口正在出售第:49张票。
二号窗口正在出售第:50张票。
二号窗口正在出售第:51张票。
一号窗口正在出售第:52张票。
二号窗口正在出售第:53张票。
一号窗口正在出售第:54张票。
一号窗口正在出售第:55张票。
二号窗口正在出售第:56张票。
一号窗口正在出售第:57张票。
二号窗口正在出售第:58张票。
一号窗口正在出售第:59张票。
二号窗口正在出售第:60张票。
一号窗口正在出售第:61张票。
二号窗口正在出售第:62张票。
二号窗口正在出售第:63张票。
一号窗口正在出售第:64张票。
一号窗口正在出售第:65张票。
二号窗口正在出售第:66张票。
二号窗口正在出售第:67张票。
一号窗口正在出售第:68张票。
一号窗口正在出售第:69张票。
二号窗口正在出售第:70张票。
二号窗口正在出售第:71张票。
一号窗口正在出售第:72张票。
一号窗口正在出售第:73张票。
二号窗口正在出售第:74张票。
二号窗口正在出售第:75张票。
一号窗口正在出售第:76张票。
一号窗口正在出售第:77张票。
二号窗口正在出售第:78张票。
一号窗口正在出售第:79张票。
二号窗口正在出售第:80张票。
一号窗口正在出售第:81张票。
二号窗口正在出售第:82张票。
一号窗口正在出售第:83张票。
二号窗口正在出售第:84张票。
一号窗口正在出售第:85张票。
二号窗口正在出售第:86张票。
二号窗口正在出售第:87张票。
一号窗口正在出售第:88张票。
一号窗口正在出售第:89张票。
二号窗口正在出售第:90张票。
一号窗口正在出售第:91张票。
二号窗口正在出售第:92张票。
一号窗口正在出售第:93张票。
二号窗口正在出售第:94张票。
二号窗口正在出售第:95张票。
一号窗口正在出售第:96张票。
一号窗口正在出售第:97张票。
二号窗口正在出售第:98张票。
二号窗口正在出售第:99张票。
一号窗口正在出售第:100张票。

              注意:同步是一种高开销的操作,因此应该尽量减少同步的内容。 

    通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 

        3、使用特殊域变量(volatile)实现线程同步

              volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新, 因此每次使用该域就要重新计算,而不是使用寄存器中的值volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

              例如:在上面的例子当中,只需在tc前面加上volatile修饰,即可实现线程同步。

/**
 * 

* Title: 多线程模拟购票窗口 

* Description: 熟练同步代码块和同步函数的使用

* @author Marmara  

* @date 2018年5月11日
 */
public class TicketSale implements Runnable{

	private volatile int tc=100;
	//private Object o1=new Object();
	public static void main(String[] args) {
		TicketSale t=new TicketSale();
		Thread t1=new Thread(t, "一号窗口");
		Thread t2=new Thread(t, "二号窗口");
		
		t1.start();
		t2.start();
		
	}

	@Override
	public void run() {
		
		while(tc>0) {
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			sale();
		}
		}
		
		
	

	public /*synchronized*/ void sale() {
		
			if(tc>0) {
				System.out.println(Thread.currentThread().getName()+"正在出售第:"+(100-tc+1)+"张票。");
			tc--;
			}
		}
		
		
	}

运行结果:

一号窗口正在出售第:1张票。
二号窗口正在出售第:1张票。
一号窗口正在出售第:3张票。
二号窗口正在出售第:3张票。
二号窗口正在出售第:5张票。
一号窗口正在出售第:5张票。
一号窗口正在出售第:7张票。
二号窗口正在出售第:8张票。
二号窗口正在出售第:9张票。
一号窗口正在出售第:9张票。
二号窗口正在出售第:11张票。
一号窗口正在出售第:11张票。
一号窗口正在出售第:13张票。
二号窗口正在出售第:13张票。
二号窗口正在出售第:15张票。
一号窗口正在出售第:15张票。
一号窗口正在出售第:17张票。
二号窗口正在出售第:17张票。
二号窗口正在出售第:19张票。
一号窗口正在出售第:19张票。
一号窗口正在出售第:21张票。
二号窗口正在出售第:21张票。
一号窗口正在出售第:23张票。
二号窗口正在出售第:23张票。
一号窗口正在出售第:25张票。
二号窗口正在出售第:25张票。
二号窗口正在出售第:27张票。
一号窗口正在出售第:27张票。
一号窗口正在出售第:29张票。
二号窗口正在出售第:29张票。
一号窗口正在出售第:31张票。
二号窗口正在出售第:31张票。
二号窗口正在出售第:33张票。
一号窗口正在出售第:34张票。
一号窗口正在出售第:35张票。
二号窗口正在出售第:35张票。
一号窗口正在出售第:37张票。
二号窗口正在出售第:37张票。
一号窗口正在出售第:39张票。
二号窗口正在出售第:40张票。
二号窗口正在出售第:41张票。
一号窗口正在出售第:41张票。
一号窗口正在出售第:43张票。
二号窗口正在出售第:43张票。
一号窗口正在出售第:45张票。
二号窗口正在出售第:45张票。
一号窗口正在出售第:47张票。
二号窗口正在出售第:47张票。
一号窗口正在出售第:49张票。
二号窗口正在出售第:49张票。
一号窗口正在出售第:51张票。
二号窗口正在出售第:51张票。
二号窗口正在出售第:53张票。
一号窗口正在出售第:54张票。
一号窗口正在出售第:55张票。
二号窗口正在出售第:55张票。
一号窗口正在出售第:57张票。
二号窗口正在出售第:57张票。
一号窗口正在出售第:59张票。
二号窗口正在出售第:59张票。
二号窗口正在出售第:61张票。
一号窗口正在出售第:61张票。
二号窗口正在出售第:63张票。
一号窗口正在出售第:63张票。
一号窗口正在出售第:65张票。
二号窗口正在出售第:65张票。
一号窗口正在出售第:67张票。
二号窗口正在出售第:68张票。
一号窗口正在出售第:69张票。
二号窗口正在出售第:69张票。
一号窗口正在出售第:71张票。
二号窗口正在出售第:71张票。
二号窗口正在出售第:73张票。
一号窗口正在出售第:73张票。
一号窗口正在出售第:75张票。
二号窗口正在出售第:75张票。
二号窗口正在出售第:77张票。
一号窗口正在出售第:77张票。
二号窗口正在出售第:79张票。
一号窗口正在出售第:79张票。
二号窗口正在出售第:81张票。
一号窗口正在出售第:81张票。
二号窗口正在出售第:83张票。
一号窗口正在出售第:84张票。
二号窗口正在出售第:85张票。
一号窗口正在出售第:85张票。
二号窗口正在出售第:87张票。
一号窗口正在出售第:87张票。
一号窗口正在出售第:89张票。
二号窗口正在出售第:89张票。
二号窗口正在出售第:91张票。
一号窗口正在出售第:91张票。
一号窗口正在出售第:93张票。
二号窗口正在出售第:93张票。
一号窗口正在出售第:95张票。
二号窗口正在出售第:95张票。
一号窗口正在出售第:97张票。
二号窗口正在出售第:98张票。
二号窗口正在出售第:99张票。
一号窗口正在出售第:99张票。

              这时发现运行结果很乱,因为volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不适用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。

              注意:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。

        4、使用重入锁实现线程同步

             在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其功能。

             ReenreantLock类的常用方法有:
                     ReentrantLock() : 创建一个ReentrantLock实例 
                     lock() : 获得锁 

                     unlock() : 释放锁 

package test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 
 * 
 * Title: 多线程模拟购票窗口
 * 
 * Description: 熟练同步代码块和同步函数的使用
 * 
 * @author Marmara
 * 
 * @date 2018年5月11日
 */
public class TicketSale implements Runnable {

	private /* volatile */ int tc = 100;
	private Lock lock = new ReentrantLock();// 声明锁
	// private Object o1=new Object();
	public static void main(String[] args) {
		TicketSale t = new TicketSale();
		Thread t1 = new Thread(t, "一号窗口");
		Thread t2 = new Thread(t, "二号窗口");

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

	}

	@Override
	public void run() {
		while (tc > 0) {
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			sale();
		}
	}

	public /* synchronized */ void sale() {

		lock.lock();
		
		try {
			if (tc > 0) {
				System.out.println(Thread.currentThread().getName() + "正在出售第:" + (100 - tc + 1) + "张票。");
				tc--;
			} 
		} finally {
			lock.unlock();
		}

	}

}

运行结果:

一号窗口正在出售第:1张票。
二号窗口正在出售第:2张票。
一号窗口正在出售第:3张票。
二号窗口正在出售第:4张票。
二号窗口正在出售第:5张票。
一号窗口正在出售第:6张票。
二号窗口正在出售第:7张票。
一号窗口正在出售第:8张票。
一号窗口正在出售第:9张票。
二号窗口正在出售第:10张票。
二号窗口正在出售第:11张票。
一号窗口正在出售第:12张票。
一号窗口正在出售第:13张票。
二号窗口正在出售第:14张票。
一号窗口正在出售第:15张票。
二号窗口正在出售第:16张票。
二号窗口正在出售第:17张票。
一号窗口正在出售第:18张票。
二号窗口正在出售第:19张票。
一号窗口正在出售第:20张票。
二号窗口正在出售第:21张票。
一号窗口正在出售第:22张票。
一号窗口正在出售第:23张票。
二号窗口正在出售第:24张票。
二号窗口正在出售第:25张票。
一号窗口正在出售第:26张票。
二号窗口正在出售第:27张票。
一号窗口正在出售第:28张票。
一号窗口正在出售第:29张票。
二号窗口正在出售第:30张票。
二号窗口正在出售第:31张票。
一号窗口正在出售第:32张票。
一号窗口正在出售第:33张票。
二号窗口正在出售第:34张票。
二号窗口正在出售第:35张票。
一号窗口正在出售第:36张票。
二号窗口正在出售第:37张票。
一号窗口正在出售第:38张票。
二号窗口正在出售第:39张票。
一号窗口正在出售第:40张票。
二号窗口正在出售第:41张票。
一号窗口正在出售第:42张票。
二号窗口正在出售第:43张票。
一号窗口正在出售第:44张票。
一号窗口正在出售第:45张票。
二号窗口正在出售第:46张票。
二号窗口正在出售第:47张票。
一号窗口正在出售第:48张票。
一号窗口正在出售第:49张票。
二号窗口正在出售第:50张票。
一号窗口正在出售第:51张票。
二号窗口正在出售第:52张票。
二号窗口正在出售第:53张票。
一号窗口正在出售第:54张票。
二号窗口正在出售第:55张票。
一号窗口正在出售第:56张票。
一号窗口正在出售第:57张票。
二号窗口正在出售第:58张票。
二号窗口正在出售第:59张票。
一号窗口正在出售第:60张票。
二号窗口正在出售第:61张票。
一号窗口正在出售第:62张票。
一号窗口正在出售第:63张票。
二号窗口正在出售第:64张票。
二号窗口正在出售第:65张票。
一号窗口正在出售第:66张票。
二号窗口正在出售第:67张票。
一号窗口正在出售第:68张票。
二号窗口正在出售第:69张票。
一号窗口正在出售第:70张票。
二号窗口正在出售第:71张票。
一号窗口正在出售第:72张票。
二号窗口正在出售第:73张票。
一号窗口正在出售第:74张票。
二号窗口正在出售第:75张票。
一号窗口正在出售第:76张票。
一号窗口正在出售第:77张票。
二号窗口正在出售第:78张票。
一号窗口正在出售第:79张票。
二号窗口正在出售第:80张票。
一号窗口正在出售第:81张票。
二号窗口正在出售第:82张票。
一号窗口正在出售第:83张票。
二号窗口正在出售第:84张票。
二号窗口正在出售第:85张票。
一号窗口正在出售第:86张票。
一号窗口正在出售第:87张票。
二号窗口正在出售第:88张票。
一号窗口正在出售第:89张票。
二号窗口正在出售第:90张票。
二号窗口正在出售第:91张票。
一号窗口正在出售第:92张票。
一号窗口正在出售第:93张票。
二号窗口正在出售第:94张票。
一号窗口正在出售第:95张票。
二号窗口正在出售第:96张票。
二号窗口正在出售第:97张票。
一号窗口正在出售第:98张票。
一号窗口正在出售第:99张票。
二号窗口正在出售第:100张票。
             注意: 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

       5.使用局部变量实现线程同步 
            如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
             ThreadLocal 类的常用方法:          

 
ThreadLocal() : 创建一个线程本地变量 
get() : 返回此线程局部变量的当前线程副本中的值 
initialValue() : 返回此线程局部变量的当前线程的"初始值" 
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value         

private static ThreadLocal<Integer> tc=new ThreadLocal<Integer>() {
		protected Integer initialValue(){  
            return 100;  
        }  
	};

    注意:ThreadLocal与同步机制 

                         ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。 

                         前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式 。

    (三) 死锁问题

               同步中嵌套同步,互相不释放。

               举例如下:

package test;
/**
 * 
 * Title: DeadLock
 * 
 * Description: 死锁问题
 * 
 * @author Marmara
 * 
 * @date 2018年5月4日
 */
public class DeadLock implements Runnable {
	public int flag = 1;
	private static Object o1 = new Object();// 静态对象是类的所有对象共享的
	private static Object o2 = new Object();
	@Override
	public void run() {
		if (flag == 1) {
			synchronized (o1) {
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (o2) {
					System.out.println("1");
				}
			}
		}
		if (flag == 0) {
			synchronized (o2) {
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (o1) {
					System.out.println("0");
				}
			}
		}
	}
	public static void main(String[] args) {
		DeadLock d1 = new DeadLock();
		DeadLock d2 = new DeadLock();
		d1.flag = 1;
		d2.flag = 0;
		new Thread(d1).start();
		new Thread(d2).start();
	}
}

参考:http://www.cnblogs.com/yezhenhan/archive/2012/01/09/2317636.html

          https://www.cnblogs.com/yjd_hycf_space/p/7526608.html

          https://blog.csdn.net/cengjingyige/article/details/52382300