线程与进程的关系
进程是一个正在执行中的程序,线程是程序执行过程中的一条分支,一个进程可以有多个线程。
创建线程的方式
• Thread子类
自定义一个类继承Thread类,重写其中run()方法。
创建该类对象并调用start()方法,就会开启一条新线程并执行run()方法中的代码。
• Runnable
自定义一个类实现Runnable接口,重启其中run()方法。
创建该类对象并创建Thread对象,在创建Thread对象时将Runnbale对象传入构造函数。
调用Thread对象的start()方法时就会开启线程运行Runnbale对象的run()方法中的代码。
Thread类常用方法
• sleep(long)
当前线程休眠指定毫秒
• currentThread()
获取当前线程对象
• getName()
获取当前线程的名字
• setName(String)
设置当前线程的名字
• setDaemon(boolean)
设置线程为守护线程,守护线程在进程中没有任何非守护线程执行时自动结束,该方法只能在线程调用start()之前使用
• join()
当前线程暂停,等待加入的线程运行结束之后继续
什么是计时器
Timer是一种工具,用其安排在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
可以通过构造函数设置其线程名及是否为守护线程。
安排任务
• schedule(TimerTask, long)
安排任务,延迟指定毫秒后执行
• schedule(TimerTask, Date)
安排任务,在指定时间执行
• schedule(TimerTask, long, long)
安排任务,延迟指定毫秒后执行第一次,之后每间隔指定毫秒重复运行
• schedule(TimerTask, Date, long)
安排任务,在指定时间时执行第一次,之后每间隔指定毫秒重复运行
练习
使用计时器安排任务,先2秒执行一次,然后4秒执行一次,再2秒执行一次,4秒执行一次,循环……
使用计时器安排任务,周一到周五每天凌晨4点执行。
同步代码块
使用synchronized(锁对象){同步代码}形式进行同步,多个线程执行同步代码块时如果使用的锁对象相同,只能有一个线程执行。
同步方法
使用synchronized关键字修饰方法,这时整个方法都是同步的,使用this作为锁对象。
静态同步方法
静态方法也可以使用synchronized关键字修饰,方法内部的代码也是同步的,这时的锁对象是当前类的Class对象。
等待
在同步代码中调用锁对象的wait()方法,可以让当前线程等待
通知唤醒
使用锁对象的notify()方法可以唤醒在该对象上等待的随机一个线程
使用锁对象的notifyAll()方法可以唤醒在该对象上等待的所有线程
练习
创建2个线程,其中一个线程内部执行3次打印,另外一个线程内部执行5次打印,第一个线程再执行3次,第二个执行5次,如此交替执行10次。
应用场景
在程序开发过程中我们经常需要在同一个线程中共享数据,例如我们常见的银行转账的案例,转入和转出是同一个线程上执行的两个方法,他们应该共享一个事务对象。
解决方案
• 自定义Map
使用一个Key对象为Thread类型的Map用来保存数据。
在存储对象时将当前线程存为Key,数据存为Value。获取对象时使用当前线程对象即可获取到线程内部共享的数据。
• ThreadLocal
Java中为我们提供了一个和当前线程相关的容器ThreadLocal。
当调用其set()方法时会将数据和当前线程绑定,调用get()方法时则是获取和当前线程绑定的数据。
一个ThreadLocal只能存储一个数据,如果有多个数据需要在线程范围内共享,可以创建多个ThreadLocal,或者将多个数据存入一个对象,将对象存入ThreadLocal。
练习
开启4个线程,线程开启后均为死循环,它们同时操作一个int变量,2个线程中对变量每次加3并且打印,另外2个线程中对变量每次减3并打印。
什么是原子类型
JDK5之后,Java中的java.util.concurrent.atomic包中提供了一些原子类型,可以对Integer、Long、Boolean和引用数据类型进行一些常用的操作,这些操作都是具有原子性的。
原子性即为不可分割的,例如我们说一组操作具有原子性,就是指这组操作的执行过程中CPU不会跳转到其他线程工作
常用API
• AtomicInteger:具有原子性的Integer
addAndGet(int) 对数字增加指定值,并且返回更新后的值
incrementAndGet() 对数字加1并且返回更新后的值
decrementAndGet() 对数字减1并且返回更新后的值
set(int) 设置为指定数值
• AtomicInteger:具有原子性的Integer数组addAndGet(int, int) 对指定索引位置上的数值增加指定值,并且返回更新后的值
incrementAndGet(int) 将指定索引上的数值加1并返回
decrementAndGet(int) 将指定索引上的数值减1并返回
set(int, int) 将指定索引上的值赋值为指定值
• AtomicIntegerFieldUpdater:Integer字段更新器
newUpdater(Class, String) 获取指定类的指定属性的更新器
addAndGet(Object, int) 将指定对象上的属性设置为指定值
什么是线程池
当我们需要执行多个任务,每个任务都需要一个线程去执行的时候,并不一定需要每次都创建一个线程,因为线程的创建和销毁都是比较消耗性能的。
我们可以事先创建一些线程,存储在一个容器池中,当需要执行任务的时候从池中获取一个线程,任务执行结束之后再将线程还回池中。
JDK5之后,Java中提供了工具类Exetors,用来创建各种线程池。ExecutorService、ScheduledExecutorService
固定大小的线程池
使用newFixedThreadPool(int) 方法创建一个固定大小的线程池,池内线程个数为指定int值
调用线程池的execute(Runnable) 方法来添加一个任务,如果池中有空闲线程,将会立即执行任务,如果池中没有空闲线程,任务将会等待池中线程完成之前的任务之后才执行
缓冲线程池
使用newCachedThreadPool () 方法创建一个缓冲线程池,池内最初没有线程
调用线程池的execute(Runnable) 方法来添加一个任务,如果池中有空闲线程,将会立即执行任务,如果池中没有空闲线程则会创建新线程执行,线程空闲60秒后销毁
单独线程池
使用newSingleThreadExecutor() 方法创建一个单独线程池,池内始终只有1个线程,如果该线程被杀死,则会重新创建
调用线程池的execute(Runnable) 方法来添加一个任务,如果池中线程空闲,将会立即执行任务,如果线程忙碌,则等待线程完成上次的任务之后才执行
计时器线程池
使用newScheduledThreadPool(int) 方法创建一个计时器线程池,池内线程数为指定int值
调用线程池的execute(Runnable) 可以添加一个立即执行的任务,原理和newFixedThreadPool(int)相同
还可以调用schedule(Runnable, long, TimeUnit) 方法来指定定时任务
Callable和Futrue
ExecutorService还可以调用submit(Callable) 方法执行一个任务,在call() 方法中定义任务内容并且返回一个值
submit方法会返回一个Future对象,在任务执行结束时,Future对象的get()方法可以得到call() 方法返回的值,如果调用get()方法时任务未完成,那么线程将阻塞等待任务完成
CompletionService
使用CompletionService可以批量添加任务之后获取最先完成的任务返回的结果
使用构造函数ExecutorCompletionService(Executor) 创建对象,然后使用submit(Callable) 方法添加任务
调用CompletionService的take() 方法可以获取到最先完成的任务返回的Future对象
ReentrantLock
使用在多线程并发时可以使用ReentrantLock对象的lock()方法开始同步,unlock()方法结束同步。
使用相同Lock对象同步的代码同一时间只能一个线程执行,原理和synchronized相同。
通常解锁的代码会放在finally中执行,避免出现一场无法解锁的问题。
ReentrantReadWriteLock
使用构造函数ReentrantReadWriteLock()可以创建读写锁对象,调用其readLock()可以获取读锁,调用writeLock()方法可以获取写锁
读锁和读锁之间不互斥,写锁和写锁之间互斥,写锁和读锁也互斥
练习
设计一个缓存容器,可以缓存多个对象。当调用缓存容器取数据时如果其中没有查询数据,则从数据库查找,如果有就直接返回。
Condition
使用Lock的newCondition()方法可以获取一个Condition,Condition对象拥有和Object类似的wait()、notify()、notifyAll()功能,分别为await()、signal()、signalAll()
区别是如果使用synchronized同步时只能使用锁对象来wait()、notify()、notifyAll(),这时notify()方法只能唤醒随机一个线程
而使用Condition时可以创建多个分支对象,让线程在不同的分支上等待,并且可以唤醒指定分支上的线程
练习
创建3个线程,其中一个线程内部执行3次打印,一个线程内部执行5次打印,另外一个线程内部执行7次.打印。第一个线程再执行3次,第二个执行5次,第三个执行7次,如此交替执行10次。
Semaphore
可以在多个线程并发时指定同时执行线程的个数。
使用构造函数Semaphore(int)创建信号灯,指定并发个数。
在线程开始执行后调用acquire()方法占用一个并发数,线程结束时使用release()释放一个并发数。
类似银行排号功能,多个客户即为多条线程,并发数量取决于银行开了几个窗口,其他客户等待前面客户办理业务结束之后排队继续。
CyclicBarrier
可以在多线程并发执行的时设置标记,等待其他线程。
使用构造函数CyclicBarrier(int)创建对象,指定等待线程的个数。
在线程开始执行之后可以使用CyclicBarrier的await()方法控制先到的线程等待,直到等待的线程到达指定个数时所有线程继续。
类似集体旅游爬山,从山脚开始爬山每个人速度不同,到达山顶的时间不同,但是先到的等待后来的一起开饭。饭后下山的速度也有所不同,先到的在车上等待后来的一起开车回家。
CountDownLatch
可以在多线程并发执行时设置等待,等待倒计时结束之后继续。
使用构造函数CountDownLatch(int)创建对象,指定倒计时次数。
在线程开始之后可以使用await()方法控制线程等待倒计时,使用countDown()方法进行倒计时,当调用countDown()到达指定次数之后await()的线程继续执行。
类似于田径赛跑,运动员等待裁判倒数开始,裁判等待运动员到达终点。
Exchanger
可以在多线程并发时设置等待,等待另一线程运行到指定位置,并且交换数据。
使用构造函数Exchanger()创建对象。
在线程开始之后可以使用exchange(Object)方法控制当前线程等待,直到有另一个线程也调用该方法时交换数据,并继续执行。
类似于买卖双方约定交易地点,其中一方先到之后等待另外一方,双方到齐之后一手交钱一手交货。
BlockingQueue
阻塞队列,可以支持多个线程向队列中添加获取元素。
BlockingQueue 为数组实现的固定大小的阻塞队列
LinkedBlockingQueue 为链表实现的不固定大小的阻塞队列
练习:使用BlockingQueue完成线程之间的通信,3个线程轮流打印
ConcurrentHashMap 线程安全的HashMap,类似于Hashtable
ConcurrentSkipListMap 线程安全的TreeMap
ConcurrentSkipListSet 线程安全的TreeSet
CopyOnWriteList 线程安全的List
第一题
现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志。
请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。
原始代码如下:
package cn.itcast.test;
import java.util.Date;
/*
* 模拟处理16行日志,下面的代码产生了16个日志对象,当前代码需要运行16秒才能打印完这些日志。
* 修改程序代码,开四个线程让这16个对象在4秒钟打完。
*/
public class Test1 {
public static void main(String[] args) {
for (int i = 0; i < 16; i++) { //这行代码不能改动
final String log = "" + (i + 1); //这行代码不能改动
Test1.parseLog(log);
}
}
public static void parseLog(String log) { //parseLog方法内部的代码不能改动
System.out.println(log + ": " + new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
第二题
程序中的Test类中的代码在不断地产生数据,然后交给TestDo.doSome()方法去处理。就好像生产者在不断地产生数据,消费者在不断消费数据。
请将程序改造成有10个线程来消费生成者产生的数据,这些消费者都调用TestDo.doSome()方法去进行处理,故每个消费者都需要一秒才能处理完。
程序应保证这些消费者线程依次有序地消费数据,同一时间只能有2个线程在消费数据,总共执行5秒。
原始代码如下:
package cn.itcast.test;
import java.util.Date;
public class Test2 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) { //这行不能改动
final String input = i + ""; //这行不能改动
System.out.println(Thread.currentThread().getName() + ": " + TestDo.doSome(input));
}
}
}
class TestDo { //不能改动此TestDo类
public static String doSome(String input) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String output = input + ": " + new Date();
return output;
}
}
第三题
TestThread类在创建对象时需要传入key和value, 当运行run()方法时会将key和value打印。
如果直接运行以下代码,4个线程会同时执行,结果如下:
2: b: Wed Nov 30 13:53:13 CST 2011
1: a: Wed Nov 30 13:53:13 CST 2011
1: d: Wed Nov 30 13:53:13 CST 2011
3: c: Wed Nov 30 13:53:13 CST 2011
要求修改代码,4个线程中如果有key相同的(equals相同,不是地址相同),则不能同时执行,需要运行出如下结果:
2: b: Wed Nov 30 13:53:13 CST 2011
1: a: Wed Nov 30 13:53:13 CST 2011
3: c: Wed Nov 30 13:53:13 CST 2011
1: d: Wed Nov 30 13:53:14 CST 2011
package cn.itcast.test;
import java.util.Date;
public class Test3 {
public static void main(String[] args) {
Thread t1 = new TestThread("1", "a");
Thread t2 = new TestThread("2", "b");
Thread t3 = new TestThread("3", "c");
Thread t4 = new TestThread("1", "d");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class TestThread extends Thread {
private Object key;
private String value;
public TestThread(Object key, String value) {
this.key = key;
this.value = value;
}
public void run() {
// 大括号以内的代码不能改动!
{
try {
Thread.sleep(1000);
System.out.println(key + ": " + value + ": " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
本文来源:https://www.2haoxitong.net/k/doc/cb94e502cc17552707220877.html
文档为doc格式