线程的实现方式:
1.类继承Thread类实现多线程
class MyThread extends Thread {
2
3 private int i = 0;
4
5 @Override
6 public void run() {
7 for (i = 0; i < 100; i++) {
8 System.out.println(Thread.currentThread().getName() + " " + i);
9 }
10 }
11 }
2.类实现Runnable接口方式
class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
3.通过Callable和FutureTask创建线程
使用Callable和Future接口创建线程。
a:创建Callable接口的实现类 ,并实现Call方法
b:创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
c:使用FutureTask对象作为Thread对象的target创建并启动线程
d:调用FutureTask对象的get()来获取子线程执行结束的返回值
public class ThreadTest {
public static void main(String[] args) {
Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
thread.start(); //线程进入到就绪状态
}
}
System.out.println("主线程for循环执行完毕..");
try {
int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<Integer> {
private int i = 0;
// 与run()方法不同的是,call()方法具有返回值
@Override
public Integer call() {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
4.通过线程池创建线程
public class ThreadDemo05{
private static int POOL_NUM = 10; //线程池数量
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i = 0; i<POOL_NUM; i++) {
RunnableThread thread = new RunnableThread();
//Thread.sleep(1000);
executorService.execute(thread);
}
//关闭线程池
executorService.shutdown();
}
}
class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
}
}
ExecutorService、Callable都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,还有Future接口也是属于这个框架,有了这种特征得到返回值就很方便了。
通过分析可以知道,他同样也是实现了Callable接口,实现了Call方法,所以有返回值。这也就是正好符合了前面所说的两种分类
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
线程池的参数配置:
public ThreadPoolExecutor(int corePoolSize, //核心线程数量,线程池创建几个固定线程来执行任务
int maximumPoolSize, //最大线程数量,当任务队列满了时,
//创建(最大线程数-核心线程数)的临时线程来执行任务
long keepAliveTime,//线程活跃时间
//临时线程执行完成后,会去任务队列获取任务,如果在活跃时间内没获取到任务,该线程会被销毁回
//(ps:核心线程默认不超时)
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,存任务放,阻塞队列
//执行策略0:先入先出,线程安全
//其次还有:ArrayBlockingQueue 基于数组的有界阻塞队列,
LinkedBlockingQueue 基于数组的无界阻塞对列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler //饱和策略,拒绝策略 ){
//饱和策略有:AbortPolicy (默认)丢弃任务,并且抛出异常
CallerRunsPolicy 不抛弃任务,让调用线程池的线程执行任务
DiscardPolicy 丢弃任务,不抛出异常
DiscardOldestPolicy 丢弃任务,不抛出异常,丢弃的是最早进入队列的任务
}
线程池的工作原理:
线程池的执行流程:
-
判断核心线程数
-
判断任务能否加入到任务队列
-
判断最大线程数量
-
根据线程池的饱和策略除了任务(是否丢弃任务)
线程的生命周期:
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
新建:就是刚使用new方法,new出来的线程;
就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
完整的生命周期图如下:
再介绍Executors(线程池工具类)类:提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int
corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
守护线程
守护线程的使用方式:
设置线程的Daemon方法为true,且必须在thread.start()之前设置
Daemon线程中产生的新线程也是Daemon
守护线程不应该去访问固有资源,如进行读写操作(文件,数据库),因为守护线程是跟随用户线程的,当没有用户线程工作时,守护线程会立即结束
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("守护线程 线程结束...");
}
});
thread.setDaemon(true); //①
thread.start();
System.out.println("用户线程 结束...");
}
}
守护线程适用场景
不适合用于输入输出或计算操作
适用于辅助用户线程场景,如GC,内存管理
线程优先级:
CPU内核同一时间智能支持能执行一个线程,如何执行多根线程会进行资源争夺,依靠线程调取器来进行分配时间片。
例如,小明被分配了多项工作任务,并且要在规定时间内交作业,小明会在任务A执行一段,瞬间又被领导叫去执行任务B,又会去执行任务C
所以在设置核心线程数一般是CPU内核数加1最合理.
java中优先级分为1-10个级别。可以使用setPriority()方法
注意:
Join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join(); //使调用线程 t 在此之前执行完毕。
t.join(1000); //等待 t 线程,等待时间是1000毫秒
线程暂停不在使用stop()方法,原因是使用stop方法强行终止线程,内部以异常的方式终止结束,推荐使用interrupt()方法中断线程。或者使用isInterrupted() 将其设置为flase
线程同步机制(重点)
1 基本概念
当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制。
多个线程并发读写同一个临界资源时会发生线程并发安全问题。
异步操作:多线程并发的操作,各自独立运行。
同步操作:多线程串行的操作,先后执行的顺序。
2 解决方案
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率。
3 实现方式
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性,具体方式如下:
使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized(类类型的引用) {
编写所有需要锁定的代码;
}
使用同步方法的方式实现所有代码的锁定。
直接使用synchronized关键字来修饰整个方法即可
该方式等价于:
synchronized(this) { 整个方法体的代码 }
4 静态方法的锁定
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){….}
那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的。
原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
5 注意事项
使用synchronized保证线程同步应当注意:
多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用。
在使用同步块时应当尽量减少同步范围以提高并发的执行效率。
6 线程安全类和不安全类
StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类。
Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
Collections.synchronizedList() 和 Collections.synchronizedMap()等方法实现安全。
7 死锁的概念
线程一执行的代码:
public void run(){
synchronized(a){ //持有对象锁a,等待对象锁b
synchronized(b){
编写锁定的代码;
}
}
}
线程二执行的代码:
public void run(){
synchronized(b){ //持有对象锁b,等待对象锁a
synchronized(a){
编写锁定的代码;
}
}
}
注意:
在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
8 使用Lock(锁)实现线程同步
(1)基本概念
从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性,在以后的线程安全控制中,经常使用ReentrantLock类显式加锁和释放锁。
(2)常用的方法
(3)与synchronized方式的比较
Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动释放。
Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好。