一、基础概念
1、CPU核心数和线程数
多核心指的是单芯片多处理器,将多个CPU集成到同一个芯片内,不同的CPU可以单独的运行程序。目前主流的CPU有四核、六核、八核。增加核心数目的是为了增加线程数,一般情况下它们是1:1对应关系,也就是说八核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。
2、CPU时间片轮转机制
我们平时在开发的时候,感觉并没有受CPU核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,是因为操作系统提供了一种CPU时间片轮转机制,时间片轮转调度使用的是RR调度算法。时间片轮转机制要注意的时间片的长度,从一个线程切换到另一个线程是需要时间的,因为需要保存核装入寄存器的值及内存映像,更新各种表格和队列等。我们把线程间的切换称为上下文切换,假设上下文切换需要的时间是5ms,如果设定的时间片是20ms,则在每次做完20ms的有用工作后,还需要花5ms来进行线程的切换,CPU时间的20%被浪费在管理开销上;可是如果设定的时间片过长,假设是500ms,这是浪费的时间只有1%,如果同时有10个交互用户按下回车键,最不幸的一个进程要等5s才能获取运行机会。所以时间片设得太短会导致过多的进程切换,降低了CPU效率;而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100毫秒通常是一个比较合理的折中。
3、进程和线程
进程是操作系统进行资源分配的最小单位,资源包括CPU、内存空间、磁盘IO等。线程是进程的一个实体,线程是CPU调度和分派的最小单位。同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程和CPU之间是没有必然关系的,因为线程是CPU调度的单位。
4、并行和并发
并行:指应用能够同时执行不同的任务。
并发:指应用能够交替执行不同的任务。当谈论并发的时候,一定要加个单位时间,也就是单位时间内并发量是多少。
两者区别:一个是同时执行,一个是交替执行。
5、高并发编程的好处
1)充分利用CPU的资源;
2)加快相应用户的时间;
3)使你的代码模块化、异步化、简单化。
注意事项:
1)线程之间的安全性(因为同一集成里面的线程资源是共享的);
2)线程之间的死循环过程;
3)线程太多会将服务器资源耗尽形成死机。
二、Java里的线程
1、线程的启动与中止
启动:
1)X extends Thread,重写run()方法;
2)X implements Runnable,实现run()方法,然后交给Thread运行;
3)X implements Callable,实现call()方法,然后交给Thread运行。
public class NewThread {
//1、拓展Thread类,覆盖run()方法
private static class UseThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("I am extends Thread");
}
}
//2、实现接口
//2.1、实现Runnable接口
private static class UseRun implements Runnable {
@Override
public void run() {
System.out.println("I am implements Runnable");
}
}
//2.2、实现Callable接口,允许有返回值
private static class UseCall implements Callable {
@Override
public String call() throws Exception {
System.out.println("I am implements Callable");
return "CallResult";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1
UseThread useThread = new UseThread();
useThread.start();
//2
UseRun useRun = new UseRun();
new Thread(useRun).start();
//3
UseCall useCall = new UseCall();
FutureTask futureTask = new FutureTask<>(useCall);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
1)、2)方式都有的一个缺陷是在执行完任务之后无法获取执行结果。从Java1.5开始,提供了Callable和Future,通过它们可以在任务执行完毕后得到任务执行的结果。Callable是一个接口,在它里面只声明了一个call()方法,call()方法返回值类型就是传递进来的V类型。
Future是对具体的Runnable或Callable任务的执行结果进行取消、查询是否完成、获取结果的。
因为Future是一个接口,所以无法直接用来创建对象使用的,因此就有了FutureTask,FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。事实上,FutureTask是Future接口的一个唯一实现类。
注意:Thread类是java语言唯一对线程的抽象,Runnable、Callable不是线程,是对任务的抽象。
中止:
如果run()方法执行完了,或者抛出了一个未处理的异常导致线程提前结束属于线程的自然终止。如果调用线程的suspend()、resume()、stop()方法来执行暂停、恢复和停止操作,属于线程的手动中止,但是这些API是过期的,也就是不建议使用,因为这些方法不会释放已经占有的资源(如锁),容易引发死锁问题,带来副作用。
安全的中止是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,但是调用了interrupt()方法并不意味着A线程就中止了,只是给A发送了中断请求,A线程是否要停止自己的工作,完全取决于A,所以A线程也可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过方法isInterrupted()或静态方法Thread.interrupted()检查自身的中断标志位是否为true来进行响应。isInterrupted()和Thread.interrupted()方法的区别是Thread.interrupted()在判断完中断标志位位true以后会再将中断标识位改为false。
public class EndThread {
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" start interrupt flag="+isInterrupted());
/**
* isInterrupted()和static方法interrupted()方法的区别:
* Thread.interrupted()在判定完中断标志位以后又把isInterrupted()置为false
*/
while (!isInterrupted()){
//while (!Thread.interrupted()){
System.out.println(threadName+" inner interrupt flag="+isInterrupted());
}
System.out.println(threadName+" finally interrupt flag="+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
//这里只是创建了一个线程对象
Thread endThread = new UseThread("endThread");
//执行了start()方法才真正的开启了线程,start()方法只能调用一次,多次调用会报一个运行时的错误
endThread.start();
Thread.sleep(20);
endThread.interrupt();
}
}
2、run()方法和start()方法
我们通过new Thread()其实只是new出来了一个Thread的实例,线程还没有启动起来,只有执行了start()方法,线程才真正的启动起来。start()方法调用了start0()native方法,start()方法让一个线程从新建状态进入就绪状态等待分配CPU,分到CPU以后才调用run()方法,start()方法不能重复调用。run()方法方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,可以被单独调用。
public class StartAndRun {
public static class ThreadRun extends Thread {
@Override
public void run() {
System.out.println(" I am " + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadRun thread = new ThreadRun();
//thread.run();//执行完以后打印的还是main线程的内容,打印结果: I am main
thread.start();//打印的是新创建的线程的内容 打印结果:I am Thread-0
}
}
3、线程常用方法
join()方法:把指定的线程加入到当前线程,可以将交替执行的线程合并为顺序执行的线程。join()方法可以不断反复插队的。比如A线程正在执行的过程中,调用了B线程的join()方法,然后B线程开始执行,在B线程执行的过程中,又调用了C线程的join()方法,然后C线程开始执行,等C线程执行结束了继续执行B线程,等B线程执行结束后再继续执行A线程。
yield()方法:是当前线程让出CPU占有权,但让出的时间是不可设定的,也不会释放锁资源,也有可能执行了yield()方法的线程在进入就绪状态后又马上进入运行状态。
sleep()方法:让当前线程休眠,休眠以后进入阻塞状态,休眠过程中不释放锁资源。
wait()方法、notify()、notifyAll()方法后边会详细讲解。
三、线程间的共享和协作
asynchronized关键字:Java支持多个线程同时访问一个对象或者对象的成员变量,asynchronized关键字可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻只能有一个线程处于方法或者同步块中,保证线程对变量访问的可见性和排他性,又称为内置锁机制。
1、线程间的共享
对象锁和类锁:
对象锁是用于对象实例方法或一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。类的对象实例可以有多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁,所以不同对象实例的类锁之间是互相影响的。需要注意的是:类锁只是一个概念上的东西,类锁其实锁的是每个类对应的class对象。
public class SynClzAndInst {
/**
* 类锁和对象锁之间互不影响
* synchronized锁的是某一个实际的对象
* 如果在普通的成员方法中加synchronized关键字,锁的是类的当前实例
* 如果在static修饰的变量或者方法中加synchronized关键字,锁的是class对象
*/
/**
* 使用类锁的线程
*/
private static class SynClass extends Thread {
@Override
public void run() {
System.out.println("TestClass is running...");
//synClass();
synStaticObject();
}
}
/**
* 类锁
*/
private static synchronized void synClass() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " synclass going...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " synClass end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 类锁
* 和上边synClass()方法的效果是一样的
* 类似于类锁,因为obj在全虚拟机只有一份
*/
private static Object obj = new Object();
public static void synStaticObject() {
synchronized (obj) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " synclass going...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " synClass end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 使用对象锁的线程
*/
private static class SynObject implements Runnable {
private SynClzAndInst synClzAndInst;
public SynObject(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance is running..." + synClzAndInst);
//synClzAndInst.instance();
synClzAndInst.instance2();
}
}
/**
* 对象锁
*/
private synchronized void instance() {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " synInstance is going..." + this.toString());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " synInstance ended " + this.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 对象锁
* 和上边instance()方法的效果是一样的
*/
private Object obj2 = new Object();
private void instance2() {
synchronized (obj2) {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " synInstance is going..." + this.toString());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " synInstance ended " + this.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst1 = new SynClzAndInst();
Thread t1 = new Thread(new SynObject(synClzAndInst1));
SynClzAndInst synClzAndInst2 = new SynClzAndInst();
Thread t2 = new Thread(new SynObject(synClzAndInst2));
t1.start();
t2.start();
// SynClzAndInst synClzAndInst1 = new SynClzAndInst();
// Thread t1 = new Thread(new SynObject(synClzAndInst1));
// Thread t2 = new Thread(new SynObject(synClzAndInst1));
// t1.start();
// t2.start();
// SynClass synClass = new SynClass();
// synClass.start();
// SynClass synClass2 = new SynClass();
// synClass2.start();
}
}
2、线程间的协作
等待/通知机制
等待和通知的标准范式:
等待方:
1)获取对象的锁
2)如果条件不满足,调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
synchronized(对象){
while(条件不满足){
对象.wait();
}
处理对应的逻辑
}
通知方:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
synchronized(对象){
改变条件
对象.notifyAll();
}
wait()和notify()、notifyAll()方法只能在同步方法或同步块中调用。在调用wait()之前,线程必须要获得该对象的对象级别锁,进入wait()方法后,当前线程释放锁,通知方和其他线程进行竞争获取锁,通知方获取到锁并改变条件,通知方释放锁(通知方线程退出调用了notifyAll的synchronized代码块)后,被唤醒的线程将会竞争获取该锁。
尽量使用notifyAll,notify只唤起一个线程,如果同时开启着多个线程,可能唤醒的不是自己希望唤醒的线程。只有一个线程的时候,才能保证唤醒的是自己希望的线程。
public class Express {
private static Express express = new Express(0, Express.CITY);
public final static String CITY = "ShangHai";
private int km;//快递运输里程数
private String site;//快递到达地点
public Express(int km, String site) {
this.km = km;
this.site = site;
}
public synchronized void changeKm() {
this.km = 101;
notifyAll();
}
public synchronized void changeSite() {
this.site = "BeiJing";
notifyAll();
}
public synchronized void waitKm() {
while (this.km < 100) {
try {
System.out.println("等待里程数大于100中。。。");
wait();
System.out.println(" check km thread [" + Thread.currentThread().getName() + "] is be notifyed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(" the km is " + this.km + ", i will change db");
}
public synchronized void waitSite() {
while (CITY.equals(this.site)) {
try {
System.out.println("等待城市不再是ShangHai中。。。");
wait();
System.out.println(" check site thread [" + Thread.currentThread().getName() + "] is be notifyed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(" the site is " + this.site + ", i will call user");
}
private static class CheckKm extends Thread {
@Override
public void run() {
express.waitKm();
}
}
private static class CheckSite extends Thread {
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
new CheckSite().start();
new CheckKm().start();
Thread.sleep(1000);
/**
* 这里如果只是更改站点,waitKm()方法也会被唤醒,
* 只不过检查了一遍条件,发现不满足,重新进入wait()方法
* 因为notifyAll()方法会唤醒所有的
*/
express.changeSite();
//express.changeKm();
}
}
最终的执行结果是:
因为使用的是notifyAll方法,所以即使代码中没有执行changeKm()方法执行更改里程数的操作,监听里程数的线程也会被唤醒,唤醒之后执行wait()方法后边的代码,然后再一次检查条件。
四、显示锁Lock
synchronized关键字是Java语言层面的锁,称之为内置锁,它的缺点是:1、不能中断,一旦某个线程进入就绪阶段,就会等待其他线程释放锁,其他线程将锁释放后,就会去抢锁,这个过程不能中断。2、没有尝试获取锁的机制,要么就是抢到了,要么就是没抢到,继续等待,不能说没抢到锁,先去处理其他事,其他事处理完了再来尝试获取锁的过程。
Lock是有Java语法层面提供的,锁的获取和释放需要我们明显的去获取。需要注意的是,一定要在finally关键字里面释放锁,如果不在finally里面,一旦抛了异常,锁就没法释放了。
Lock是一个接口,里面的几个核心方法是:
方法名称 | 描述 |
void lock() | 获取锁,调用该方法当前线程将会获取锁; 获取到锁后,从该方法返回 |
void lockInterruptibly() throws InterruptedException | 可中断的获取锁,和lock的区别在于该方法会 响应中断,即在锁的获取中可以中断当前线程,抛出异常 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立刻返回。 如果能获取到返回true,否则返回false。 |
boolean tryLock(long time,TimeUnit unit) throws InterruptedException | 超时的获取锁: 1、当前线程超时时间内获取到锁,返回true 2、超时时间内被中断,抛出异常 3、超时时间结束,返回false |
void unlock() | 释放锁 |
1、可重入锁ReentrantLock
允许一个线程反复多次的去拿同一把锁,一般在加锁的递归方法中会用到。ReentrantLock和synchronized都是可重入锁。
public class LockDemo {
private int count = 0;
private Lock lock = new ReentrantLock();
/**
* 对于非可重入锁,如果这样反复的去拿同一把锁,这个线程会自己把自己锁死。
*/
public void incr() {
lock.lock();
try {
while (count < 10) {
System.out.println(count);
count++;
incr();
}
} finally {
lock.unlock();
}
}
public synchronized void incr2(){
while (count<10){
System.out.println(count);
count++;
incr2();
}
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
//lockDemo.incr();
lockDemo.incr2();
}
}
2、公平和非公平锁
如果在时间上,先对锁进行获取的请求一定先获取到锁,那么这个锁是公平的,反之,是不公平的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。synchronized是非公平锁。
事实上,公平的锁机制没有非公平的锁机制效率高。原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。与此同时,如果C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
3、读写锁ReentrantReadWriteLock
上边提到的那些锁,都是排他锁,这些锁在同一时刻只允许一个线程进行访问。而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程均被阻塞。读写锁维护了两个锁,一个读锁和一个写锁。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
class Student{
private String name="default";
private int age=0;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public interface StudentService {
public Student getStudent();
public void setStudentName(String name);
}
public class UseSyn implements StudentService {
private Student student;
public UseSyn(Student student) {
this.student = student;
}
@Override
public synchronized Student getStudent() {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.student;
}
@Override
public void setStudentName(String name) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
student.setName(name);
}
}
public class UseRwLock implements StudentService{
private Student student;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock();//读锁
private final Lock setLock = lock.writeLock();//写锁
public UseRwLock(Student student) {
this.student = student;
}
@Override
public Student getStudent() {
getLock.lock();
try {
Thread.sleep(5);
return this.student;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
getLock.unlock();
}
}
@Override
public void setStudentName(String name) {
setLock.lock();
try {
Thread.sleep(5);
student.setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
setLock.unlock();
}
}
}
public class BusiApp {
//读操作
private static class GetThread implements Runnable {
private StudentService studentService;
public GetThread(StudentService studentService) {
this.studentService = studentService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
studentService.getStudent();
System.out.println(Thread.currentThread().getName() + "读学生信息耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
//写操作
private static class SetThread implements Runnable {
private StudentService studentService;
public SetThread(StudentService studentService) {
this.studentService = studentService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
studentService.setStudentName("张韶涵");
System.out.println(Thread.currentThread().getName() + "写学生信息耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
public static void main(String[] args) {
Student student = new Student();
StudentService studentService = new UseSyn(student);
//StudentService studentService = new UseRwLock(student);
for (int i = 0; i < 3; i++) {
Thread setT = new Thread(new SetThread(studentService));
for (int j = 0; j < 10; j++) {
Thread getT = new Thread(new GetThread(studentService));
getT.start();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
setT.start();
}
}
}
运行BusiApp类,创建UseSyn对象和创建UseRwLock对象所需要的时间对比:
4、Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
public class ExpressCond {
public final static String CITY = "ShangHai";
private int km;
private String site;
private Lock lock = new ReentrantLock();
private Condition kmCond = lock.newCondition();
private Condition siteCond = lock.newCondition();
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
public void changeKm() {
lock.lock();
try {
this.km = 101;
kmCond.signalAll();
} finally {
lock.unlock();
}
}
public void changeSite() {
lock.lock();
try {
this.site = "BeiJing";
siteCond.signalAll();
} finally {
lock.unlock();
}
}
public void waitKm() {
lock.lock();
try {
while (this.km < 100) {
System.out.println("等待里程数大于100中。。。");
kmCond.await();
System.out.println(" check km thread[" + Thread.currentThread().getName() + "] is be notifyed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(" the Km is " + this.km + " , I will change db");
}
public void waitSite() {
lock.lock();
try {
while (CITY.equals(this.site)) {
System.out.println("等待城市不再是ShangHai中。。。");
siteCond.await();
System.out.println(" check site thread[" + Thread.currentThread().getName() + "] is be notifed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(" the site is " + this.site + ", I will call user");
}
}
public class TestCond {
private static ExpressCond expressCond = new ExpressCond(0, ExpressCond.CITY);
private static class CheckKm extends Thread {
@Override
public void run() {
expressCond.waitKm();
}
}
private static class CheckSite extends Thread {
@Override
public void run() {
expressCond.waitSite();
}
}
public static void main(String[] args) {
new CheckKm().start();
new CheckSite().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
expressCond.changeSite();
//expressCond.changeKm();
}
}
上边代码的执行结果是:
执行结果没有像上边三.2的结果一样监听里程数变化的线程也被唤醒是因为这里等待、改变里程数和等待、改变地点用的是不同的等待/通知Condition对象。
同样,如果把signalAll改为signal,就会只有一个线程被唤醒。
将ExpressCond类中的changeSite()方法中的signalAll改为signal
public void changeSite() {
lock.lock();
try {
this.site = "BeiJing";
siteCond.signal();
} finally {
lock.unlock();
}
}
TestCond类中的main方法中多开几个监听里程数变化的线程,
public static void main(String[] args) {
for(int i=0;i<3;i++){
new CheckSite().start();
}
new CheckKm().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
expressCond.changeSite();
//expressCond.changeKm();
}
最后的执行结果是:
可以看到,最终只有一个线程被唤醒。