传统线程机制
一. 传统使用类Thread和接口Runnable实现
Java中线程的创建有两种方式:
① 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中。
② 通过实现Runnable接口,实例化Thread类。
⒈ 通过Thread类实现
⑴ 第一种写法:继承Thread类
// 继承Thread类
private static class MyThread extends Thread
{
String name = null;
int ticket = 0;
public MyThread(String name)
{
this.name = name;
}
public synchronized void run()
{
for (int i = 0; i < 5; i++ )
{
System.out.println(
Thread.currentThread().getName() + this.name + " ticket:" + ticket++ );
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
// 测试
public static void main(String[] args)
{
MyThread mThread1 = new MyThread("线程一");
MyThread mThread2 = new MyThread("线程二");
mThread1.start();
mThread2.start();
}
⑵ 第二种写法:在Thread子类重写run()方法中编写运行代码
new Thread()
{
@Override
public void run()
{
while (true)
{
try
{
Thread.sleep(2000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}.start();
⒉ 通过接口Runnable类实现
⑴ 第一种写法:实现Runnable接口
// 实现Thread类
private static class RunThread implements Runnable
{
int Counter = 0;
@Override
public synchronized void run()
{
for (int i = 0; i < 5; i++ )
{
System.out.println(Thread.currentThread().getName() + "count:" + Counter++ );
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
// 测试
public static void main(String[] args)
{
RunThread rThread = new RunThread();
Thread t1 = new Thread(rThread, "线程一");
Thread t2 = new Thread(rThread, "线程二");
t1.start();
t2.start();
}
⑵ 第二种写法:在传递给Thread对象的Runnable对象的run()方法中编写代码
new Thread(new Runnable(){
public void run(){
while(true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}).start();
二. 定实现时器 Timer 和 TimerTask
Timer 在实际开发中应用场景不多,一般来说都会用其他第三方库来实现。但有时会在一些面试题中出现。下面我们就针对一道面试题来使用 Timer 定时类。
要求:模拟写出双重定时器。使用定时器,间隔 4 秒执行一次,再间隔 2 秒执行一次,以此类推执行。
package com.ithao.main;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TraditionalTimerTest
{
private static int count = 0;
public static void main(String[] args)
{
class MyTimerTask extends TimerTask
{
public void run()
{
count = (count + 1) % 2;
System.out.println("bombing!");
new Timer().schedule(new MyTimerTask(), 2000 + 2000 * count);
}
}
new Timer().schedule(new MyTimerTask(), 2000);
while (true)
{
System.out.println(new Date().getSeconds());
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
三. 线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
直接相互制约。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理, 那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步。对于互斥可以这样理解,线程 A 和线程 B 互斥访问某个资源则它们之间就会产个顺序问题——要么线程 A 等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其实就 是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
package com.ithao.main;
/**
* 子线程运行执行10次后,主线程再运行5次。这样交替执行三遍
*/
public class DemoTest
{
public static void main(String[] args)
{
final Business bussiness = new Business();
// 子线程
new Thread(new Runnable()
{
public void run()
{
for (int i = 0; i < 3; i++ )
{
bussiness.subMethod();
}
}
}).start();
// 主线程
for (int i = 0; i < 3; i++ )
{
bussiness.mainMethod();
}
}
}
// 这个类是执行任务的类
class Business
{
private boolean flag = false;
// flag为true,主线程执行
public synchronized void mainMethod()
{
// 是不是子线程在执行?是,继续等子线程执行完
while (false == flag)
{
try
{
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 执行主线程任务
for (int i = 0; i < 5; i++ )
{
System.out.println(Thread.currentThread().getName() + i);
}
// 轮到子线程执行了
flag = false;
// 唤醒子线程。(一共就两个线程,子线程和主线程,所以唤醒的只能是子线程。)
notify();
}
// flag为false,子线程执行
public synchronized void subMethod()
{
// 是不是主线程在执行?是,继续等主线程执行完
while (true == flag)
{
try
{
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 执行子线程任务
for (int i = 0; i < 10; i++ )
{
System.out.println(Thread.currentThread().getName() + i);
}
// 轮到主线程执行了
flag = true;
// 唤醒主线程。(一共就两个线程,子线程和主线程,所以唤醒的只能是主线程。)
notify();
}
}
四. 线程局部变量ThreadLocal
1. ThreadLocal 的作用和目的
用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程的 ThreadLocalMap 对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set 方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap 对象,该对象的 Key 是 ThreadLocal 对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
2. ThreadLocal 的应用场景
2.1. 订单处理包含一系列操作
减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
2.2. 银行转账包含一系列操作
把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
2.3. 例如 Strut2 的 ActionContext
同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext 方法拿到的对象都不相同,对同一个线程来说,不管调用 getContext 方法多少次和在哪个模块中 getContext 方法,拿到的都是同一 个。
3. ThreadLocal 的使用方式
ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
3.1. 在关联数据类中创建 private static ThreadLocal
在下面的类中,私有静态 ThreadLocal 实例(serialNum)为调用该类的静态 SerialNum.get() 方法的每个线程维护了一个“序列号”,该方法将返回当前线程的序列号。(线程的序列号是在第一次调用 SerialNum.get() 时分配的,并在后续调用中不会更改。)
package com.ithao.main;
public class SerialNum {
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
其他案例:
public class ThreadContext {
private String userId;
private Long transactionId;
private static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected ThreadContext initialValue() {
return new ThreadContext();
}
};
public static ThreadContext get() {
return threadLocal.get();
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Long getTransactionId() {
return transactionId;
}
public void setTransactionId(Long transactionId) {
this.transactionId = transactionId;
}
}
3.2. 在Util类中创建ThreadLocal
这是上面用法的扩展,即把ThreadLocal的创建放到工具类中。
【例】例如Hibernate的工具类:
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义SessionFactory
static {
try {
// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 获取当前线程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
3.3. 在Runnable中创建ThreadLocal
还有一种用法是在线程类内部创建ThreadLocal,基本步骤如下:
1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
public class ThreadLocalTest implements Runnable{
ThreadLocal<Studen> studenThreadLocal = new ThreadLocal<Studen>();
@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);
Studen studen = getStudent(); //通过这个方法,为每个线程都独立的new一个student对象,每个线程的的student对象都可以设置不同的值
studen.setAge(age);
System.out.println(currentThreadName + " is first get age: " + studen.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + studen.getAge());
}
private Studen getStudent() {
Studen studen = studenThreadLocal.get();
if (null == studen) {
studen = new Studen();
studenThreadLocal.set(studen);
}
return studen;
}
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 Studen{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
五. 多线程共享数据
在 Java 传统线程机制中的共享数据方式,大致可以简单分两种情况:
多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个 Runnable对象,这个Runnable 对象中有那个共享数据,例如,卖票系统就可以这么做。
多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同的Runnable 对象。例如,银行存取款。
1. 多个线程行为一致共同操作一个数据
如果每个线程执行的代码相同,可以使用同一个 Runnable 对象,这个 Runnable 对象中有那个共享数据。
package com.ithao.main;
public class SellTickets {
public static void main(String[] args) {
ShareData shareData = new ShareData();//共享的数据对象
for (int i = 0; i < 4; i++) {
new Thread(new RunnableCusToInc(shareData), "Thread " + i).start();
}
}
}
/**
* 共享数据类
**/
class ShareData {
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();
}
}
}
/**
*多线程类
**/
class RunnableCusToInc implements Runnable {
private ShareData shareData;//共享的数据对象
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
2. 多个线程行为不一致共同操作一个数据
如果每个线程执行的代码不同,这时候需要用不同的 Runnable 对象,有如下两种方式来实现这些 Runnable对象之间的数据共享:
2.1. 将共享数据封装在另外一个对象中
然后将这个对象逐一传递给各个 Runnable 对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
package com.ithao.main;
public class BankWithdrawals {
public static void main(String[] args) {
ShareData2 shareData = new ShareData2();
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
new Thread(new RunnableCusToInc2(shareData), "Thread " + i).start();
} else {
new Thread(new RunnableCusToInc3(shareData), "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();
}
}
}
/**
* 多线程类
**/
class RunnableCusToInc2 implements Runnable {
private ShareData2 shareData;
public RunnableCusToInc2(ShareData2 data) {
this.shareData = data;
}
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
class RunnableCusToInc3 implements Runnable {
private ShareData2 shareData;
public RunnableCusToInc3(ShareData2 data) {
this.shareData = data;
}
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
System.out.println("33333333333333333333333");
}
}
}
2.2. 将这些 Runnable 对象作为某一个类中的内部类
共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable 对象调用外部类的这些方法。
package com.ithao.main;
public class BankWithdrawals {
public static void main(String[] args) {
// 公共数据
final ShareData shareData=new ShareData();
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}, "Thread " + i).start();
} else {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
shareData.dec();
}
}
}, "Thread " + i).start();
}
}
}
static class ShareData {
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();
}
}
}
}
补充:上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的 Runnable 对象 作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容 易实现它们之间的同步互斥和通信。