一、线程的实现方式
a) 实现Runnable接口(推荐)
b) 继承Thread类
c) 实现Callable接口(需要和线程池一起使用)
二、线程部分方法理解
a) setPriority(int newPriority):一定程度上影响线程对cpu资源的占有优先级
b) sleep(long millis):静态方法,在休眠时间到达后,重新回到就绪状态,供操作系统调配
c) interrupt():将该线程标记为中断状态,在遭遇到线程阻塞时(线程在调用Object类的wait方法,或者该类的join方法或sleep方法过程中受阻),则其中断状态将被清除,它还将收到一个InterruptedException。但是,run方法仍然会继续执行。我在BigJava这本书里面看到,为了实现interrupt的中断效果,所以要执行的代码写在try语句类。例如:
public class MyRunnableimplements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep();
//...代码
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
d) join():加入当前线程,并在该线程执行完之后,当前线程才会继续执行。 我画了一张图可以帮助理解:
main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程
e) yield() :让出本次获得的cpu资源,并且处于就绪状态,供操作系统调配
三、线程同步
a) 线程安全的原因
首先我们模拟线程安全问题的经典买票程序:
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
@Override
public void run() {
while (true) {
// t1,t2,t3三个线程
// 这一次的tickets = 100;
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
<pre name="code" class="java">public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
} 运行该程序会发现有两个问题。一是会出现卖了相同的票,二是票出现了负数。产生该问题的原因:
i.相同的票:CPU的一次操作必须是原子性的(可以理解为最小操作单位)。也就是ticket--实际上是两个原子性的操作(将ticket的值告诉程序和将ticket的值减一),即当程序运行到ticket--时,假设此时打印正在出售第100张票,还未来的及将ticket的值减一,该线程就得到cpu资源的时间已到,所以另一个线程进来时仍然打印正在出售第100张票
ii.出现负数票:随机性和延迟导致的。假设卖到还剩一张票时,t1线程运行到Thread.sleep(100)时,t2线程抢到cpu资源,同样运行到Thread.sleep(100)时,t3线程抢到cpu资源,由于此时三个线程都进入了if结构,也就产生了负数票
b) 线程安全问题的前提
i. 多线程环境
ii. 有共享数据
iii. 有多条语句操作共享数据
c)线程同步
i.目的:解决线程安全问题
ii.解决方案:
方案一:(1)使用synchronized关键字将多条语句操作共享数据的代码锁住,该方案利用的是每个对象都有自身唯一的一个锁。注意:锁住的对象必须是同一个对象。关键代码:
synchronized(this){
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
由于是实现Runnable接口来新建线程(
SellTicket st = new SellTicket();),所以三个线程都有一个共同的对象st。因此在本例中可以直接用this关键字来表示该相同
对象
(2)将多条语句操作共享数据的代码用方法封装,再用synchronized关键字修饰该方法。注意:该方法默认锁住的是所在类的this对象,若方法是静态方法,则锁住的是 类名.class 对象
方案二:使用lock锁(JDK1.5以后的新方法)。关键代码如下
public class SellTicket implements Runnable {
// 定义票
private int tickets = 100;
// 定义锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
} finally {
// 释放锁
lock.unlock();
}
}
}
}
四、线程通信
生产者消费问题
该问题的实质是在现实生活中,如果一个线程用来生产物品,另一个线程用来消费该物品,则需要考虑到实际情况。一方面生产线程不能生产超过实际容量的物品,同样的,消费线程在生产的物品消费完后,也不能继续消费。解决该问题的方法就是利用被锁住对象的wait(),
notify(),
notifyAll()
方法。根据实际情况当该线程需要等待时,便调用wait()方法,同样的,在生产完或消费完后边调用notify()或notifyAll()方法唤醒其它线程。注意,notify()方法是唤醒在等待池中的随机单个线程,而notifyAll是唤醒等待池中的所有线程。
五、线程池
i.线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
ii.如何实现线程池的代码
A:调用Executors类的方法创建线程池。例如创建一个带有指定线程个数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
B:调用ExcutorService类的submit方法执行线程的代码,结束后,重新回到线程池成为空闲状态
C:调用ExcutorService类的shutdown方法启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
这里用实现线程的第三种方式实现Callable(V)接口,注意与Runable接口不同的是:
A:该方法必须和线程池共同使用
B:该线程要重写的方法是call()方法
C:call()方法有一个返回值,返回类型是该方法的泛型V
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
return null;
}
}
public class CallableDemo {
public static void main(String[] args) {
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyCallable());
pool.submit(new MyCallable());
//结束
pool.shutdown();
}
}
六、线程生命周期图
注:该图是引用传智播客刘意老师的。本博客的实例也是引用刘意老师的
七、定时器类
Timer类:指定任务及该任务执行的时间和频率。
TimerTask:制定任务。该类也是实现了Runnable接口,需要重写run()方法。
使用实例:
// 做一个任务
class MyTask extends TimerTask {
private Timer t;
public MyTask(){}
public MyTask(Timer t){
this.t = t;
}
@Override
public void run() {
System.out.println("beng,爆炸了");
t.cancel();
}
}
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
// 3秒后执行爆炸任务
// t.schedule(new MyTask(), 3000);
//结束任务
t.schedule(new MyTask(t), 3000);
}
}
因为本例中是要任务只要实施一次,而计时器run方法结束后不会自动终止。所以将Timer的对象传入MyTask类中(如果直接在main中调用有可能引发线程安全问题)调用
cancel()方法终止计时器。