Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
public class MyThread2 implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
public class Main {
public static void main(String[] args){
MyThread3 th = new MyThread3(); //callable接口实现类实例
//使用FutureTask类来包装Callable对象
FutureTask future = new FutureTask(th);
Thread oneThread = new Thread(oneTask); //用FutureTask对象实例化thread对象
oneThread.start();
try{
System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
}catch(Exception e){
ex.printStackTrace();
}
}
}
(1)实现Runnable和实现Callable接口的方式基本相同,可以把这两种方式归为一种,这种实现接口的方式与直接继承Thread类的方法之间的差别如下:
注:一般推荐采用实现接口的方式来创建多线程
(2)Callable与Runnable区别
Callable执行call()方法可以有返回值,并且可以声明抛出异常
boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
boolean isDone():若Callable任务完成,返回True
boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
Thread类的静态方法,用于让当前正在执行的线程暂停一段时间,并进入阻塞状态,直到休眠时间结束,才进入就绪态。sleep将给其他线程执行机会,不理会其优先级。
Thread.sleep(1000); //暂停1000ms
注:与Object类方法Wait()的区别:
Thread类的静态方法,也可以当前正在执行的线程暂停,但不进入阻塞状态,而是直接转入就绪状态。当某线程调用yield方法暂停之后,只有优先级大于等于该线程并处于就绪状态的线程可以运行。因此,可能出现某线程调用yield方法暂停后,调度器又将其调度出来重新执行的情况。
此外,与sleep不同,yield没有声明抛出异常。
t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,(T1占用A,想要B,T2占用B想要A)若无外力作用,它们都将无法推进下去。死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
• 互斥条件:一个资源每次只能被一个进程使用。
• 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
• 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
• 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
synchronized关键字最主要有以下3种应用方式(锁住的总是对象)
// 修饰实例方法
public synchronized void method()
{
// todo
}
// 修饰静态方法
public synchronized static void method() {
// todo
}
// 修饰代码块
public static void test() {
//类锁
synchronized (A.class) {
System.out.println("haha");
}
}
public void test2() {
//实例锁
synchronized (this) {
System.out.println("haha");
}
}
所谓等待唤醒机制主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
其中,常用的ReentrantLock(可重入锁)特点具体如下:
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try
{
//需要同步的代码...
}
finally{
lock.unlock();
}
private final ReentrantLock lock = new ReentrantLock();
private final Condition cond = lock.newCondition();
lock.lock();
...
cond.await();
...
cond.signalAll();
...
lock.unlock();
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。JAVA通过线程池使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。
当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
ThreadPoolExecutor → AbstractExecutorService(抽象类)→ ExecutorService(接口)→ Executor(顶层接口)
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)8=32。
这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) CPU数目
用于避免多个线程对共享资源(变量)的竞争
当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
Thread 在内部是通过ThreadLocalMap来维护ThreadLocal变量表, 在Thread类中有一个threadLocals 变量,是ThreadLocalMap类型的,它就是为每一个线程来存储自身的ThreadLocal变量的, ThreadLocalMap是ThreadLocal类的一个内部类,这个Map里面的最小的存储单位是一个Entry, 它使用ThreadLocal对象作为key, 要存储的变量值作为 value,所以在每一个线程里面,可能存在着多个ThreadLocal变量
数据库连接、Session管理
public class ConnectionManager {
private static ThreadLocal connThreadLocal = new ThreadLocal();
public static Connection getConnection() {
if(connThreadLocal.get() != null)
return connThreadLocal.get();
//获取一个连接并设置到当前线程变量中
Connection conn = getConnection();
connThreadLocal.set(conn);
return conn;
}
}
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap
虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,我们有两种手段。
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
1) 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
原理:
2) 禁止进行指令重排序,即:
i++操作的非原子性:当线程执行这个语句时,1)会先从主存当中读取i的值,然后复制一份到高速缓存当中,2)然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,3)最后将高速缓存中i最新的值刷新到主存当中。
volatile只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
例:假如某个时刻变量inc的值为10
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1。
forkjoinpool支持将一个任务拆分成多个小任务并行计算,再把多个小任务的结果并成总的计算结果
举例来说:对超过1000万个元素的数组进行排序,这种任务本身可以并发执行,但如何拆解成小任务需要在任务执行的过程中动态拆分。这样,大任务可以拆成小任务,小任务还可以继续拆成更小的任务,最后把任务的结果汇总合并,得到最终结果,这种模型就是Fork/Join模型。
class SumTask extends RecursiveTask {
static final int THRESHOLD = 100;
long[] array;
int start;
int end;
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
SumTask subtask1 = new SumTask(this.array, start, middle); //建两个小任务
SumTask subtask2 = new SumTask(this.array, middle, end);
invokeAll(subtask1, subtask2); //N-1个任务会使用fork()交给其它线程执行,但是,它还会留一个任务自己执行
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
);
return result;
}
}