一篇文章了解java并发基础

一.线程基础

1. 线程的基本概念

线程,即轻量级进程,是程序执行流的最小单元,一个标准的线程由线程id,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程不拥有系统资源,只拥有少量运行必须的资源。

2. 线程的生命周期和五种基本状态

参考文章:https://blog.csdn.net/peter_teng/article/details/10197785
在这里插入图片描述

  1. 新建状态:当用new操作符创建一个线程时,例如new Thread®,线程还没有开始运行,此时线程处在新建状态,当一个线程处于新生状态时,程序还没有开始运行线程中的代码

  2. 就绪状态:一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run方法,当start()方法返回后,线程就处于就绪状态。

注意点:处于就绪状态的线程并不一定立刻运行run方法,线程还必须同其他线程竞争cpu时间,只有获取cpu时间才可以运行线程。因为在单cpu的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态,然而此时可能有多个线程处于就绪状态,对多个处于就绪状态的线程时由java运行时系统的线程调度程序(threadscheduler)来调度的。

  1. 运行状态(running):当线程获取cpu时间后,它才进入运行状态,真正开始执行run()方法。

  2. 阻塞状态(Blocked):
    阻塞状态就是正在运行的线程还没有运行结束,暂时让出cpu,这时处于就绪状态的线程就可以获取cpu时间,进入运行状态。

线程运行过程中,可能由于以下各种原因进入阻塞状态。

  1. 线程通过调用sleep方法进入睡眠状态;
  2. 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回它的调用者;
  3. 线程试图获取一个锁,但是该锁正被其他线程持有。
  4. 线程在等待其他某个触发条件
    。。。
  1. 死亡状态
    为了确认线程在当前是否存活着(要么时可运行的,要么就是被阻塞了),需要使用isAlive方法判断,如果时可运行或被阻塞,返回true,如果线程死亡就返回false。

有两种方法会导致线程死亡:

  1. run方法正常退出而自然死亡。
    2.有未捕获的异常终止了run方法,使线程猝死。

3. 线程互斥与同步

参考文章:https://www.cnblogs.com/mithrandirw/p/8942335.html

参考文章:https://blog.csdn.net/a673786103/article/details/83930506

  1. 线程互斥

1)概念:多个线程之间有共享资源时会出现互斥现象,线程互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

2)临界段:

  • 概念:多线程互斥使用共享资源的程序段,在操作系统中统称临界段,临界段是一种加锁的机制,与多线程共享临界资源有关,在JAVA中使用关键字synchronized定义临界段,能对共享对象进行上锁操作。
  • 作用:临界段的作用是在任何时刻一个共享资源只能供一个线程使用。当资源未被占用,线程可以进入处理这个资源的临界段,从而得到该资源的使用权;当线程执行完毕,便退出临界段。如果一个线程已进入某个共享资源,并且还没有使用结束,其他线程必须等待。
  • 调度原则:①如果有若干线程要求进入空闲的临界区,一次仅允许一个线程进入。②任何时候,处于临界区内的线程不可多于一个。如已有线程进入自己的临界区,则其它所有试图进入临界区的线程必须等待。③进入临界区的线程要在有限时间内退出,以便其它线程能及时进入自己的临界区。④如果线程不能进入自己的临界区,则应让出CPU,避免线程出现“忙等”现象。

3)需要使用例子:设有若干个线程共享某个变量,而且都对变量有修改,如果他们之间不考虑相互协调工作,就会产生混乱,比如,线程A和B共同变量x,都对x进行+1操作,由于A和B没有协调,两线程对x的读取,修改和写入操作相互交叉,可能两个线程读取相同个x值,一个线程将修改后的x新值写入到x后,另一个线程也把自己对x修改后的新值写入到x。这样,x只记录后一个线程的修改作用。

  1. 线程同步

1)概念:当线程A使用某个对象,而此对象又需要线程B修改后才能符合本线程的需要,此时线程A就要等待线程B完成修改工作。这种线程相互等待称为线程的同步。
2)技巧:

为实现同步,JAVA语言提供了wait()、notify()和notifyAll()三个方法供线程在临界段中使用。
在临界段中使用wait()方法,使执行该方法的线程等待,并允许其他线程使用这个临界段。wait()常用两种格式:
wait()——让线程一直处于等待队列,知道被使用了notify()或notifyAll()方法唤醒。
wait(long timeout)——让线程等待到被唤醒,或经过指定时间后结束等待。
当线程使用完临界段后,用notify()方法通知由于想使用这个临界段而处于等待状态的线程结束等待。notify()方法只是通知第一个处于等待的线程。
如果某个线程在使用完临界段方法后,其他早先等待的线程都可以结束等待,一起重新竞争CPU,则可以使用notifyAll()方法。

4. java线程常用方法

参考文章:https://blog.csdn.net/tongxuexie/article/details/80142638
https://blog.csdn.net/Kaka534/article/details/51849285?depth_1-

首先来一张关系图
在这里插入图片描述
接下来上代码

1. 新建线程的三种方法


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//1. 使用继承Thread类创建线程
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("自己创建的线程");
    }
}

//2.以Runnable接口创建线程
/*
Thread类的核心功能就是进行线程的启动,但一个类为了实现多线程直接取继承Thread类时出现的问题就是:单继承的局限性!所以Java中还提供了另一种实现多线程的方法:实现Runnable接口来创建多线程。
*/

class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("利用Runnable接口创建线程");
    }
}

//3.实现Callable接口创建线程
/*
Runnable接口的run()方法没有返回值,而Callable接口的call()方法有返回值,如果某些线程执行完成后需要返回值的话,就需要用Callable接口创建线程。
*/
class MyThread2 implements Callable<String> {
    @Override
    public String call() throws Exception{
        return "Callable接口创建线程";
    }
}

public class Start {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        /*第一种方法*/
        //实例化一个对象
        MyThread myThread=new MyThread();
        //调用Thread类的start()方法
        myThread.start();
        //在main方法中打印一条语句
        System.out.println("main方法");


        /*第二种方法*/
        //实例化Runnable接口的对象,其实也可以实例化MyThread类的对象,因为可以向上转型
        Runnable runnable=new MyThread();//也可以改为 MyThread1 runnable=new MyThread();
        //实例化Thread类的对象
        Thread thread=new Thread(runnable);
        //调用Thread类的start()方法
        thread.start();
        //main线程中打印的一条语句
        System.out.println("main方法");


        /*第三种方法*/
        //1.利用MyThread类实例化Callable接口的对象
        Callable callable=new MyThread2();
        //2.利用FutureTask类的构造方法public  FutureTask(Claaable<V> callable)
        //将Callable接口的对象传给FutureTask类
        FutureTask task=new FutureTask(callable);
        //3.将FutureTask类的对象隐式地向上转型
        //从而作为Thread类的public Thread(Runnable runnable)构造方法的参数
        Thread thread1=new Thread(task);
        //4.调用Thread类的start()方法
        thread1.start();
        //FutureTask的get()方法用于获取FutureTask的call()方法的返回值,为了取得线程的执行结果
        System.out.println(task.get());

    }
}

2. sleep()方法:

定义:线程休眠:指的是让线程暂缓执行,等到预计时间之后再恢复执行。

(1)线程休眠会交出CPU,让CPU去执行其他的任务。

(2)调用sleep()方法让线程进入休眠状态后,sleep()方法并不会释放锁,即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象。

(3)调用sleep()方法让线程从运行状态转换为阻塞状态;sleep()方法调用结束后,线程从阻塞状态转换为可执行状态。

代码例子

class MyThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<5;i++)
		{
			//使用Thread类的sleep()方法,让线程处于休眠状态
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("当前线程:"+Thread.currentThread().getName()+"-----i="+i);
		}
	}
}
public class Test1 {
	public static void main(String[] args){
		MyThread myThread=new MyThread();
		//利用myThread对象分别创建三个线程
		Thread thread1=new Thread(myThread);
		thread1.start();
		Thread thread2=new Thread(myThread);
		thread2.start();
		Thread thread3=new Thread(myThread);
		thread3.start();
	}
}

3. yield()方法

定义:线程让步:暂停当前正在执行的线程对象,并执行其他线程。

1):会让当前线程交出cpu权限,让cpu去执行其他线程。
2):和sleep方法类型,不会释放锁,但不能控制具体交出cpu的时间。
3):yield()方法只能让拥有相同优先级的线程获取cpu执行的机会
4):跟sleep不同,不是让线程进入阻塞状态,而时让线程从运行状态转变为就绪状态,只需要等待重新获取cpu执行的机会即可

使用例子

class MyThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<5;i++)
		{
			//使用Thread类的yield()方法
			Thread.yield();
			System.out.println("当前线程:"+Thread.currentThread().getName()+"-----i="+i);
		}
	}
}
public class Test1 {
	public static void main(String[] args){
		MyThread myThread=new MyThread();
		//利用myThread对象分别创建三个线程
		Thread thread1=new Thread(myThread);
		thread1.start();
		Thread thread2=new Thread(myThread);
		thread2.start();
		Thread thread3=new Thread(myThread);
		thread3.start();
	}
}

4. join()方法

  • 定义:等待线程终止:指的是如果在主线程中调用该方法时就会让主线程休眠,让调用join方法的线程下执行完毕再开始主线程

  • 技巧:

1)A线程中调用B线程的join方法,那么A线程需要等待B线程执行完成后才能完成

2)主线程中依次调用A线程的join方法,B线程的join方法,可以保证A,B线程顺序执行;

3)是主线程进入等待状态,子线程在运行,子线程运行完成后会通知主线程继续运行,或者join也可以设置主线程的等待时间,当主线程等待超时时,即使子线程没有运行完,主线程也会开始继续执行,

测试代码

class MyThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<2;i++)
		{
			//使用Thread类的sleep()方法
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("当前线程:"+Thread.currentThread().getName()+"-----i="+i);
		}
	}
}
public class Test1 {
	public static void main(String[] args) throws InterruptedException{
		MyThread myThread=new MyThread();
		Thread thread1=new Thread(myThread,"自己创建的线程");
		thread1.start();
		System.out.println("主线程:"+Thread.currentThread().getName());
		//线程对象thread1调用join()方法
		thread1.join();
		System.out.println("代码结束");
	}
}

执行结果

主线程:main
当前线程:自己创建的线程-----i=0
当前线程:自己创建的线程-----i=1
代码结束

如果不加join方法的话,运行结果如下

主线程:main
代码结束
当前线程:自己创建的线程-----i=0
当前线程:自己创建的线程-----i=1

5. isAlive()

定义:线程处于“新建”状态时,线程调用isAlive()方法返回false。在线程的run()方法结束之前,即没有进入死亡状态之前,线程调用isAlive()方法返回true.

6. interrupt()

一个占有CPU资源的线程可以让休眠的线程调用interrupt()方法“吵醒”自己,即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。

注意点:
1.一个对象在多次调用start方法时,会出现Expection in thread “main” java.lang.IllegalThreadStateExpection异常
2. 启动一个线程的唯一方法就是调用Thread类的start()方法

二.Java语言层面支持的可见性实现原理方式:

参考文章:1)https://blog.csdn.net/zjy15203167987/article/details/82531772)https://www.cnblogs.com/qypx520/p/5852016.html

所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。

1.Synchronized的使用

  1. 使用的原因:1)存在共享的变量。2)多线程共同操作共享数据

  2. 效果:可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时,可以保证一个线程的变化可见

  3. 三种应用方式:
    1):普通同步方法(实例方法),锁是当前实例对象,进入同步代码前要获取当前实例的锁
    2):静态同步方法,锁是当前类的class对象,进入同步代码前要获取当前类对象的锁
    3):同步代码块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获取给定对象的锁
    4.作用:解决并发问题的一种最简单的方法,可以确保线程互斥的访问同步代码。

  4. 举例子
    1):多个线程访问同一个对象的同一个方法

public class SynchronizedTest implements Runnable{
    //共享的资源
    static int i=0;
    static int j=0;

    //synchronized修饰的实例方法
    public synchronized void increase(){
        i++;
    }

    public void increasej(){
        j++;
    }

    @Override
    public void run(){
        for (int j = 0; j <10000 ; j++) {
            increase();
            increasej();
        }
    }

    public static void main(String[] args)throws InterruptedException {
        SynchronizedTest synchronizedTest=new SynchronizedTest();
        Thread t1=new Thread(synchronizedTest);
        Thread t2=new Thread(synchronizedTest);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i为"+i+"j为"+j);
    }
}

结果

i为30000j为29927

结果解析:j的增加方法由于没有加锁,所以其实是一个随机值,而i的increase方法有加锁,一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,但是可以访问非synchronized修饰的方法

2):同步代码块
定义:为什么要同步代码块呢?在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。

 public class synchronizedTest implements Runnable {
    static synchronizedTest instance=new synchronizedTest();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<10000;j++){
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

2.volitile的使用

参考文章:
1):https://www.jianshu.com/p/ccfe24b63d87
2):https://www.cnblogs.com/chengxiao/p/6528109.html

1. 特性:
1):保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说时立即可见的。(实现可见性)
2):禁止进行指令重排序。(实现有序性)
3):只能保证对单次读/写的原子性。i++这种操作不能保证原子性
4):一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级。

2. 与synchronized做对比
看一下下面的例子

package test;

import java.util.concurrent.CountDownLatch;

/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

执行结果并不是300000。
问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:
1.读取
2.加一
3.赋值
所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。

3. 禁止指令重排序优化

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

三:相关经典题目

银行家算法(这里有一篇文章写的很好,建议大家直接去看一下):https://blog.csdn.net/wyf2017/article/details/80068608

发布了36 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/s_xchenzejian/article/details/100512251