一、多线程基础知识--传统线程机制的回顾
1、传统使用类Thread和接口Runnable实现
1):在Thread子类覆盖的run方法中编写运行代码
2):在传递给Thread对象的Runnable对象的run方法中编写代码
3):总结
查看Thread类的run()方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread的run方法没有被覆盖,并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法。
2、定时器Timer和TimerTask
Timer在实际开发中应用场景不多,一般来说都会用其他第三方库来实现。但有时会在一些面试题中出现。
1):模拟双重定时器
3、线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源是,如多个线程急用同一台打印机,会是打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要内容是使并发执行的各线程之间能够有效的共享资源和互相合作,从而使程序的执行具有可再现性
当线程并发执行时,由于资源共享和线程协作,使得线程之间会存在以下两种制约关系。
1):间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
2):直接相互制约。这种制约主要时因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将会处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产生顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥、互斥其实是一种特殊的同步。
例子:
4、线程局部变量ThreadLocal
A:ThreadLocal的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
B:每个线程调用全局ThreadLocal对象的set方法,在set方法中,首先根据当前线程获取当前线程的ThreadLocalMap对象,然后往这个map中插入一条记录,key其实是ThreadLocal对象,value是各自的set方法传进去的值。也就是每个线程其实都有一份自己独享的ThreadLocalMap对象,该对象的key是ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
C:ThreadLocal的应用场景:
➢ 订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
➢ 银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
➢ 例如 Strut2 的 ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext 方法拿到的对象都不相同,对同一个线程来说,不管调getContext 方法多少次和在哪个模块中 getContext 方法,拿到的都是同一个。
2、在Util类中创建ThreadLocal
3、在Runnable中创建ThreadLocal
1):在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离的对象xxx。
2):在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null的时候,应该new一个隔离访问类型的对象,并强制转换为要应用的类型。
3):在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
import java.util.Random; public class ThreadLocalTest implements Runnable{ ThreadLocal<Student> studentThreadLocal = new ThreadLocal<>(); @Override public void run() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName+"is running..."); Random random = new Random(); int age = random.nextInt(100); System.out.println(currentThreadName+"is set age:"+age); //通过这个方法,为每个线程都独立的new一个student对象,每个线程的student对象都可以设置不同的值 Student student = getStudent(); student.setAge(age); System.out.println(currentThreadName+"is first get age:"+student.getAge()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(currentThreadName+"is second get age:"+student.getAge()); } private Student getStudent() { Student student = studentThreadLocal.get(); if(null==student){ student = new Student(); studentThreadLocal.set(student); } return student; } public static void main(String[] args) { ThreadLocalTest t = new ThreadLocalTest(); Thread t1 = new Thread(t,"Thread A "); Thread t2 = new Thread(t,"Thread B "); t1.start(); t2.start(); } } class Student{ private String name; private Integer age; public Student(String name, Integer age) { this.name = name; this.age = age; } public Student() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
运行结果如下:
Thread B is running... Thread A is running... Thread B is set age:70 Thread A is set age:75 Thread A is first get age:75 Thread B is first get age:70 Thread A is second get age:75 Thread B is second get age:70
5、多线程共享数据
在Java传统机制中的共享数据方式,大致可以简单分为两种情况:
1):多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统。
2):多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这是需要用不用的Runnable对象。例如,银行存取款。
下面我们通过两个示例代码来分别说明这两种方式:
1、多个线程行为一致共同操作一个数据
如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统。
/** * 共享数据类 */ class ShareData1 { private int num = 10; public synchronized void inc() { num++; System.out.println(Thread.currentThread().getName() + ": invoke inc method num = " + num); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 多线程类 */ public class RunnableCusToInc implements Runnable { private ShareData1 shareData; public RunnableCusToInc(ShareData1 shareData) { this.shareData=shareData; } @Override public void run() { for (int i = 0; i < 5; i++) { shareData.inc(); } } /** * 测试方法 */ public static void main(String[] args) { ShareData1 shareData = new ShareData1(); for(int i=0;i<4;i++){ new Thread(new RunnableCusToInc(shareData),"Thread"+i).start(); } } }
运行结果如下:
Thread0: invoke inc method num = 11 Thread0: invoke inc method num = 12 Thread0: invoke inc method num = 13 Thread0: invoke inc method num = 14 Thread0: invoke inc method num = 15 Thread3: invoke inc method num = 16 Thread3: invoke inc method num = 17 Thread3: invoke inc method num = 18 Thread3: invoke inc method num = 19 Thread3: invoke inc method num = 20 Thread2: invoke inc method num = 21 Thread2: invoke inc method num = 22 Thread2: invoke inc method num = 23 Thread2: invoke inc method num = 24 Thread2: invoke inc method num = 25 Thread1: invoke inc method num = 26 Thread1: invoke inc method num = 27 Thread1: invoke inc method num = 28 Thread1: invoke inc method num = 29 Thread1: invoke inc method num = 30
2、多个线程行为不一致共同操作一个数据
如果每个线程执行的代码不同,这时候需要用不用的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享。
1)将共享数据封装在另外一个对象中,然后将这个对象逐一传递个各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
/** * 共享数据类 */ class ShareData1 { private int num = 10; public synchronized void inc() { num++; System.out.println(Thread.currentThread().getName() + ": invoke inc method num = " + num); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void dec() { num--; System.out.println(Thread.currentThread().getName() + ": invoke dec method num = " + num); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 测试方法 */ public static void main(String[] args) { ShareData1 shareData = new ShareData1(); for (int i = 0; i < 4; i++) { if (i % 2 == 0) { new Thread(new RunnableCusToInc(shareData), "Thread" + i).start(); } else { new Thread(new RunnableCusToDec(shareData), "Thread" + i).start(); } } } } /** * 多线程类 */ class RunnableCusToInc implements Runnable { //封装共享数据 private ShareData1 shareData; public RunnableCusToInc(ShareData1 shareData) { this.shareData = shareData; } @Override public void run() { for (int i = 0; i < 5; i++) { shareData.inc(); } } } class RunnableCusToDec implements Runnable { //封装共享数据 private ShareData1 shareData; public RunnableCusToDec(ShareData1 shareData) { this.shareData = shareData; } @Override public void run() { for (int i = 0; i < 5; i++) { shareData.dec(); } } }
运行结果如下:
Thread0: invoke inc method num = 11 Thread0: invoke inc method num = 12 Thread0: invoke inc method num = 13 Thread0: invoke inc method num = 14 Thread0: invoke inc method num = 15 Thread3: invoke dec method num = 14 Thread3: invoke dec method num = 13 Thread3: invoke dec method num = 12 Thread3: invoke dec method num = 11 Thread3: invoke dec method num = 10 Thread2: invoke inc method num = 11 Thread2: invoke inc method num = 12 Thread2: invoke inc method num = 13 Thread2: invoke inc method num = 14 Thread2: invoke inc method num = 15 Thread1: invoke dec method num = 14 Thread1: invoke dec method num = 13 Thread1: invoke dec method num = 12 Thread1: invoke dec method num = 11 Thread1: invoke dec method num = 10
2):将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
public class InnerThreadRunnable { public static void main(String[] args) { final ShareData2 shareData = new ShareData2(); for(int i=0;i<4;i++){ if(i%2==0){ new Thread(new Runnable() { @Override public void run() { shareData.inc(); } },"Thread"+i).start(); }else{ new Thread(new Runnable() { @Override public void run() { shareData.dec(); } },"Thread"+i).start(); } } } } class ShareData2{ private int num = 10 ; public synchronized void inc() { num++; System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void dec() { num--; System.err.println(Thread.currentThread().getName()+": invoke dec method num =" + num); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果如下:
Thread0: invoke inc method num =11 Thread3: invoke dec method num =10 Thread2: invoke inc method num =11 Thread1: invoke dec method num =10
补充:上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的 Runnable 对象作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。