并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多 个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创 建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程 中是可以有多个线程的,这个应用程序也可以称之为多线程程序。 简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
创建线程类
Java使用java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是 完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。 Java中通过继承Thread类来创建并启动多线程的步骤如下:
1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把 run()方法称为线程执行体。
2. 创建Thread子类的实例,即创建了线程对象
3. 调用线程对象的start()方法来启动该线程
方法类:
public class MyThread extends Thread{
//通过构造函数对线程进行命名
public MyThread(String name) {
super(name);
}
@Override
//设置线程任务,即线程执行体
public void run() {
for (int i = 0; i <5 ; i++) {
System.out.println(this.getName()+"--->"+i);
}
}
}
测试类:
public class demo01 {
public static void main(String[] args) {
/创建三个线程
MyThread p1 = new MyThread("p1");
p1.start();
MyThread p2 = new MyThread("p2");
p2.start();
MyThread p3 = new MyThread("p3");
p3.start();
}
}
运行结果:
p2--->0
p3--->0
p3--->1
p1--->0
p3--->2
p2--->1
p2--->2
p2--->3
p2--->4
p3--->3
p1--->1
p3--->4
p1--->2
p1--->3
p1--->4
从上面可以看到,线程是交替运行的,也就是因为java中是抢占式调度。那个线程抢到了CPU那个线程就执行。
多线程原理:
- 程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的 start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
- 通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。
- 多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
- 多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
Thread类
-
构造方法
public Thread() :分配一个新的线程对象。 public Thread(String name) :分配一个指定名字的新的线程对象。 public Thread(Runnable target) :分配一个带有指定目标新的线程对象。 public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
-
常用方法
public String getName() :获取当前线程名称。 public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。 public void run() :此线程要执行的任务在此处定义代码。 public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。 public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
创建线程的第二种方法
- 采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。
- 步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
- 调用线程对象的start()方法来启动线程。
代码实现:
public class MyRunnable implements Runnable {
@Override
//设置线程任务
public void run() {
for(int i = 0 ;i < 5 ;i ++ ){
//返回当前线程的名称和i
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
public class demo02 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
//通过Thread类的构造方法,创建线程t来执行线程任务
Thread t = new Thread(mr,"Runnable");
t.start();
}
}
运行结果:
Runnable--->0
Runnable--->1
Runnable--->2
Runnable--->3
Runnable--->4
Thread和Runnable的区别
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
扩展::在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
匿名内部类创建线程
public class demo03 {
public static void main(String[] args) {
//匿名内部类实现Runnable接口
Runnable r =new Runnable(){ //多态
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
};
Thread t = new Thread(r);
t.start();
//匿名内部类实现Thread类
new Thread(){
@Override
public void run() {
for (int i = 0; i < 10 ; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}.start();
}
}
线程安全
- 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。
案例:实现买票窗口
- 第一种方法:同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
public class ticketRunnable implements Runnable {
private int ticket = 100 ;
Object object= new Object();
@Override
public void run() {
while (ticket > 0) {
synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
- 第二种方法:同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。
public class ticketRunnable implements Runnable {
private int ticket = 100 ;
@Override
public void run() {
while (ticket > 0) {
payTicket();
}
}
public synchronized void payTicket(){
if (ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 第三种方法:java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ticketRunnable implements Runnable {
private int ticket = 100 ;
//ReentrantLock实现了Lock接口
Lock l =new ReentrantLock();
@Override
public void run() {
while (ticket > 0) {
//得到锁
l.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放锁
l.unlock();
}
}
}
}
}
Obejct类中的方法
- void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 - void notify()
唤醒在此对象监视器上等待的单个线程。会继续执行wait方法之后的代码。
进入到TimeWaiting(计时等待)有两种方式
1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,
还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
void notify() 唤醒在此对象监视器上等待的单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
线程通信
- 为什么要处理线程间通信:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们 希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。 - 如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就 是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效 的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制
- 什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是 故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时 候你们更多是一起合作以完成某些任务。 - 就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将 其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
- wait/notify 就是线程间的一种协作机制。
调用wait和notify方法需要注意的细节:
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
互斥锁
-
1.同步
* 使用ReentrantLock类的lock()和unlock()方法进行同步
-
2.通信
* 使用ReentrantLock类的newCondition()方法可以获取Condition对象 * 需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法 * 不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了
多个线程的通信:
package Threaddemo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class Thread_communicate extends Thread{
public static void main(String[] args) {
final printer p = new printer();
new Thread(){
@Override
public void run() {
while(true) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true) {
try {
p.print3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
class printer{
private int flag = 1 ;
//创建对象
private ReentrantLock r = new ReentrantLock();
//三个线程有三种不同的状态
private Condition c1 = r.newCondition();
private Condition c2 = r.newCondition();
private Condition c3 = r.newCondition();
public void print1() throws InterruptedException {
//lock()方法获得锁与unlock()方法释放锁实现同步
r.lock();
if(flag != 1){
c1.await();
}
System.out.println("窝窝头,一块钱四个");
flag = 2 ;
//唤醒c2
c2.signal();
r.unlock();
}
public void print2() throws InterruptedException {
r.lock();
if (flag != 2) {
c2.await();
}
System.out.println("谁TM买小米!");
flag = 3 ;
c3.signal();
r.unlock();
}
public void print3() throws InterruptedException {
r.lock();
if (flag != 3) {
c3.await();
}
System.out.println("菠菜,菠菜,贱卖!");
flag = 1 ;
c1.signal();
r.unlock();
}
}
窝窝头,一块钱四个
谁TM买小米!
菠菜,菠菜,贱卖!
窝窝头,一块钱四个
谁TM买小米!
菠菜,菠菜,贱卖!
窝窝头,一块钱四个
谁TM买小米!
菠菜,菠菜,贱卖!
线程池
- 程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内 存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
-
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程 池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
-
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优 的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官 方建议使用Executors工程类来创建线程池对象。
-
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) 返回线程池对象。(创建的是有界线 程池,也就是池中的线程个数可以指定最大数量)
-
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task) : 获取线程池中的某一个线程对象,并执行 Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
import java.util.concurrent.*;
//Object泛型,也可以是其他,string,Integer都行
class Thread_Callable implements Callable<String> {
@Override
//能够抛出异常
public String call() throws Exception {
String s = "妈妈,我想吃烤山药";
return Thread.currentThread().getName()+s ;
}
}
class demo_Callable{
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个有两个线程的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//如果没有返回值,直接调用submit()方法调用就行
Future ft = es.submit(new MyThread());
//上面这个线程没有返回值,所以返回null
System.out.println(ft.get());
//有返回值时就需要用Future接收,再调用get()方法
Future<String> ft1 = es.submit(new Thread_Callable());
System.out.println(ft1.get());
//关闭线程池
es.shutdown();
//这种方法很少用,一般配合线程池使用,如上
FutureTask<String> ft2 = new FutureTask<String>(new Thread_Callable());
Thread t = new Thread(ft2);
t.start();
System.out.println(ft2.get());
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
}
运行结果:
Thread-0--->0
Thread-0--->1
Thread-0--->2
Thread-0--->3
Thread-0--->4
null
pool-1-thread-2妈妈,我想吃烤山药
Thread-1妈妈,我想吃烤山药