一、 基础知识
并发与并行:
- 并发:指两个或多个事件在同一时间间隔内发生。
- 并行:指两个或多个事件在同一时刻发生。
线程与进程:
- 进程:进程是一个具有某个功能的程序在计算机中的一次动态执行过程,是操作系统进行资源分配和调度的一个独立单位。因为它是资源分配的一个独立单位,所以每个进程都有一个独立的内存空间。
- 线程:首先线程是进程中的一个执行单元。在早期的OS中并没有线程的概念,进程是拥有资源和独立运行的最小单位。后来,由于计算机的发展,对CPU的要求越来越高,而且进程之间的切换开销较大(因为每个进程都有一个独立的内存空间)。为了更好的满足需求,就发明了线程。让线程成为CPU调度的最小单位,而一个进程可以包括多个线程,多个线程共享一个内存空间,这样我们进行线程切换的开销要比进程的切换要小得多。
总结:
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
举个例子:我们运行着的一个QQ就是一个进程,我们可以在QQ上进行视频聊天、发语音和文字聊天等功能,这里的三个功能就对应着进程中的线程。
进程与线程的关系图.png二、多线程
创建线程:
Java使用 java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java中创建一个新的线程有两种方法。一个是通过继承Thread类来创建并启动多线程,具体步骤如下 :
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
代码如下:
测试类:
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
自定义线程类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
//重写run方法,完成该线程执行的逻辑
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
多线程原理:程序启动运行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()
:返回对当前正在执行的线程对象的引用。
前面讲了创建线程有两种方式,一种是继承Thread类方式,还有一种就是实现Runnable接口方式。
实现Runnable接口来创建线程
具体步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
- 调用线程对象的start()方法来启动线程。
代码示例如下:
public class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i=0;i<20;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
public class Demo03Runnable{
public static void main(String[] args) {
RunnableImpl run=new RunnableImpl();
//创建线程对象
Thread t=new Thread(run);
t.start();
for (int i=0;i<20;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现 Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
使用匿名内部类来实现线程的创建
public class Demo04InnerClassThread {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + "程序员");
}
}
};
new Thread(r).start();
}
}
三、线程安全
所谓的线程安全,就是指在多个线程同时运行的情况下,程序仍能按照我们期望的那样运行下去。
举个例子:
public class GetCount implements Runnable{
private Integer count=0;
@Override
public void run() {
while (true){
count++;
System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
//创建线程任务对象
GetCount gc=new GetCount();
//创建三个线程
Thread t1=new Thread(gc,"线程一");
Thread t2=new Thread(gc,"线程二");
Thread t3=new Thread(gc,"线程三");
//同时访问count变量
t1.start();
t2.start();
t3.start();
}
}
运行结果:
线程一正在访问count,count为:2
线程三正在访问count,count为:3
线程二正在访问count,count为:2
......
我们可以看到,这里出现了两个2,这种情况明显就是不符合我们的预期,也就是所谓的线程不安全。出现这种问题的原因有很多,最常见的就是,当线程一在进入方法后,拿到了count的值,刚把该值读取出来,但还没有进行count++操作,线程二就进来了,结果导致线程一和线程二拿到的count值是一样的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。
线程同步
为了解决上面所讲的线程安全问题,java为我们提供了同步机制(synchronized)来解决。
这里我们先讲一下异步和同步的关系。
- 异步:由于OS中的进程是并发执行的,所以进程以不可预知的速度向前推进。内存中的每个进程何时执行,何时暂停,以怎样的速度向前推进,每道程序总共需要多少时间才能完成等,都是不可预知的。
- 同步:同步是为了解决异步产生的问题,使得程序最终能够按照我们预期的那样执行。
java中提供了三种方式来完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
- 锁对象可以是任意类型。
- 多个线程对象要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。
修改线程任务类的代码:
public class GetCount implements Runnable{
private Integer count=0;
Object lock=new Object();
@Override
public void run() {
while (true){
synchronized (lock){
count++;
System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
线程一正在访问count,count为:1
线程三正在访问count,count为:2
线程二正在访问count,count为:3
线程一正在访问count,count为:4
线程二正在访问count,count为:5
线程三正在访问count,count为:6
线程一正在访问count,count为:7
线程三正在访问count,count为:8
线程二正在访问count,count为:9
线程一正在访问count,count为:10
线程二正在访问count,count为:11
线程三正在访问count,count为:12
线程一正在访问count,count为:13
线程二正在访问count,count为:14
线程三正在访问count,count为:15
......
使用了同步代码块后,上述的线程安全问题就解决了。
同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
修改线程任务类:
public class GetCount implements Runnable{
private Integer count=0;
public synchronized void getCount(){
count++;
System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
}
@Override
public void run() {
while (true){
getCount();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
线程一正在访问count,count为:1
线程三正在访问count,count为:2
线程二正在访问count,count为:3
线程一正在访问count,count为:4
线程三正在访问count,count为:5
线程二正在访问count,count为:6
线程三正在访问count,count为:7
线程二正在访问count,count为:8
......
同步方法同样解决了线程不安全的问题。
Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,将加锁与释放锁方法化了,如下:
-
public void lock()
:加同步锁。 -
public void unlock()
:释放同步锁。
使用如下:
public class GetCount implements Runnable{
private Integer count=0;
Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
count++;
System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
lock.unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
线程三正在访问count,count为:1
线程二正在访问count,count为:2
线程一正在访问count,count为:3
线程二正在访问count,count为:4
线程一正在访问count,count为:5
线程三正在访问count,count为:6
线程二正在访问count,count为:7
线程一正在访问count,count为:8
线程三正在访问count,count为:9
......
四、线程状态和线程池
线程状态概述
在线程的生命周期中,拥有六种状态。在api中java.lang.Thread.State
这个枚举中给出了六种线程状态。
我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态即可。
等待和唤醒机制
前面我们谈到的都是线程之间的竞争,比如去争夺锁。下面我们讲讲线程之间的协作机制,即等待唤醒机制。
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中。
- notify:则选取所通知对象的 wait set 中的一个线程释放;
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意: 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
举例:生产者与消费者问题
包子资源类:
public class BaoZi {
String pier ;
String xianer ;
boolean flag = false ;//包子资源是否存在
}
吃货线程类:
public class ChiHuo extends Thread{
private BaoZi bz;
public ChiHuo(BaoZi bz){
this.bz=bz;
}
@Override
public void run() {
while (true){
synchronized (bz){
if (bz.flag==false){
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后
System.out.println("吃货正在吃:"+bz.pi+bz.xian+"包子");
bz.flag=false;
bz.notify();
System.out.println("吃货已经把:"+bz.pi+bz.xian+"包子吃完了,包子铺开始生产包子");
System.out.println("--------------------------");
}
}
}
}
包子铺线程类:
public class BaoZiPu extends Thread{
private BaoZi bz;
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
@Override
public void run() {
int count=0;
while (true){
synchronized (bz){
if (bz.flag==true){
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (count%2==0){
//生产薄皮三鲜馅包子
bz.pi="薄皮";
bz.xian="三鲜馅";
}else {
//生产 冰皮 牛肉大葱馅
bz.pi="冰皮";
bz.xian="牛肉大葱馅";
}
count++;
System.out.println("包子铺正在生产:"+bz.pi+bz.xian+"包子");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bz.flag=true;
bz.notify();
System.out.println("包子铺已经生产好了:"+bz.pi+bz.xian+"包子,吃货可以开吃了");
}
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
BaoZi bz=new BaoZi();
new BaoZiPu(bz).start();
new ChiHuo(bz).start();
}
}
执行结果:
包子铺正在生产:薄皮三鲜馅包子
包子铺已经生产好了:薄皮三鲜馅包子,吃货可以开吃了
吃货正在吃:薄皮三鲜馅包子
吃货已经把:薄皮三鲜馅包子吃完了,包子铺开始生产包子
--------------------------
包子铺正在生产:冰皮牛肉大葱馅包子
包子铺已经生产好了:冰皮牛肉大葱馅包子,吃货可以开吃了
吃货正在吃:冰皮牛肉大葱馅包子
吃货已经把:冰皮牛肉大葱馅包子吃完了,包子铺开始生产包子
--------------------------
......
线程池
前面我们每次使用线程的时候就去创建一个线程,这会导致一个问题。比如说系统中并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建和销毁线程就会大大降低系统的效率。
因此java中提供了一种可以复用线程的方法(线程池),就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。
- 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。
线程池的使用
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接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。
- 提交Runnable接口子类对象。
- 关闭线程池(一般不用)。
示例代码如下:
public class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
}
}
public class Demo01ThreadPool {
public static void main(String[] args) {
//创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
//从线程池中获取线程对象,然后调用其的run()方法
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
//关闭线程池
es.shutdown();
}
}
网友评论