Java多线程、同步安全

目录

程序、进程、线程

 CPU的单核、多核

并发、并行

多线程的创建、使用

Thread类

常用方法

多线程创建方式一:继承于Thread类

 给线程命名:

线程优先级

  多线程创建方式二:实现Runnable接口

两种方式的对比

多线程创建方式三:有返回值:Callable接口

Callable接口的使用

多线程创建方式四:线程池

线程的生命周期

线程安全问题

线程同步synchronized

synchronized同步代码块

 因此,同步代码块既不能框的太少,也不能框的太多

synchronized同步方法

死锁

公平锁、非公平锁

jdk5之后提供了ReentrantLock类

wait和notify、notifyAll

wait和sleep的区别

为什么同步代码块不能框多也不能框少

线程通信:生产者消费者源码与解析


程序、进程、线程

程序:静态的代码,没有运行起来的才叫程序

eg:文件夹中的所有文件

整个文件夹内包括的所有静态代码的集合就是程序

进程:是程序的一次运行or正在运行的一个程序,是一个动态的过程,并且有自身的产生、存在、消亡的过程——生命周期

eg:运行中的QQ、运行中的LOL、运行中的360安全卫士

线程:进程可以细分为线程,是程序内部的一条执行路径。如果一个进程可以同时并行执行多个线程,就是支持多线程的。

线程是调度和执行的单位,有独立的运行栈和程序计数器,线程切换的开销小

一个进程中多个线程共享相同的资源,因此可以进程间通信更高效,但也带来了安全隐患

eg:同时扫漏洞、扫毒、清理垃圾就是多线程,多线程增加了cpu利用率

eg:图形化界面的基本都是多线程

一个java应用程序java.exe至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程

 CPU的单核、多核

单核CPU:实际上是一种假的多线程,表象的多线程实际上是快速在不同线程之间切换(时分复用),是一种并发

eg:同一个进程,使用单核cpu,单线程比多线程更快,因为没有线程切换时间

多核CPU:可以实现并行

eg:现在的很多手机都是8核心,但这8核并不是完全相同,当手机写备忘录时,调用功耗小功能更弱的核,玩游戏时调用更强但功耗大的核

并发、并行

并发:一个CPU同时执行多个线程

并行:多个CPU同时执行多个线程

多线程的创建、使用

java语言JVM允许程序运行多个线程,通过java.lang.Thread类来体现

Thread类

 查API文档

 JDK定义了接口Runnable,内含run()方法,这个方法可以做任何事,需要重写

线程Thread类是Runnable的实现

 Thread类包含了多个属性和方法

 

常用方法

  • void start(): 启动线程,并执行对象的run()方法
  • void run(): 线程在被调度时执行的操作
  • String getName(): 返回线程的名称
  • void setName(String name):设置该线程名称
  • static Thread currentThread(): 返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类
  • static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法。
  • join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行。该方法需要try-catch
  • static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出InterruptedException异常。该方法需要try-catch
  • stop(): 强制线程生命期结束,不推荐使用
  • boolean isAlive():返回boolean,判断线程是否还活着

 线程休眠:sleep

public static void sleep(long millis)throws InterruptedException
设置休眠的毫秒数,一到时间自动唤醒
public static void sleep(long millis,int nanos)throws InterruptedException
设置休眠的毫秒数和纳秒数,一到时间自动唤醒

线程中断:interrupt

public boolean isInterrupted()
//m1.isInterrupted()判断m1是否中断
public void interrupt()
//m1.isterrupt中断m1

线程强制执行:join

Thread类中的join()方法原理

  • 一般是用在main方法中,用于占用主线程。
  • 副线程必须先start再join才有效
  • join方法的本质就是用调用方线程给主线程main上锁直到该线程结束才释放
  • 遇到join时,主线程立即被占用,不会存在安全问题,因为本质就是同步机制
  • 如果同时存在两个以上的副线程,并且同时对main用join上锁,那么main线程的恢复执行以最后一次锁结束为准,不同线程之间不受影响,因为他们占用的都是main线程而没有彼此占用
class J1Thread extends Thread{
    @Override
    public void run() {
        for(int i=1;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"\t11111111111");
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class J2Thread extends Thread{
    public void run() {
        for(int i=1;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"\t22222222222");
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        J1Thread j1 = new J1Thread();//创建线程对象
        J2Thread j2 = new J2Thread();//创建线程对象
        for(int i =1;i<100;i++) {//主线程main真正开始的地方
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"\t主线程开始"+i);
        }//执行完for循环体才会轮得到后面j1的开始
        j1.start();//先创建j1的线程
        j1.join();//用j1给主线程main上锁,知道j1结束才释放
        for(int i =1;i<100;i++) {//主线程main继续
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"\t主线程中途"+i);
        }//执行完for循环体才会轮得到后面j2的开始
        j2.start();
        for(int i =1;i<100;i++) {
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"\t主线程结束"+i);
            if(i==90)//for循环体途中用join上锁
       j2.join();//然后j2占用main线程,把剩余部分执行完毕
        }
    }
}

 线程让步:yield()

本质:多线程并发执行是多个线程交替执行,对于某一个线程并不会执行一下就马上释放,而是会持续一个随机的很短的时间。因此通过yield方法可以强制让正在占用资源的线程让出资源,让包括自己在内的所有线程去争夺资源。因此

  • yield没有join那么强硬,join是直接上锁,yield只是放弃资源让大家重新争夺
  • yield也没有sleep那么稳定,yield有极小概率没有任何让步效果,而sleep则稳定延迟
  • 执行次数足够多时yield也能明显看到让步效果
  • 同样的yield无论写多少个都一样,而同样的sleep写10个就有10倍的延迟效果
class MyThread1 implements Runnable{
    public void run() {
        for (int i = 100; i < 200; i++) {
           System.out.println("11111111礼让");
               yield();
        }
    }
}

class YouThread1 implements Runnable{
    public void run() {
        for (int i = 100; i < 200; i++) {
            System.out.println("22222222");

        }
    }
}
public class YiledTest {
    public static void main(String[] args) {
        MyThread1 M1 = new MyThread1();
        YouThread1 Y1 = new YouThread1();
        Thread T1 = new Thread(M1);
        Thread T2 = new Thread(Y1);

        T1.start();
        T2.start();
    }
}

这一礼让基本把自己让没了

多线程创建方式一:继承于Thread类

  1. 创建一个Thread的子类MyThread
  2. 重写run()方法
  3. new一个Mythread的对象
  4. 利用对象调用start()方法运行

如果最终是用对象调用run()方法,则没有创建多线程,执行的顺序还是单线程的顺序

 例如:

public class ThreadTest {
    public static void main(String[] args) {
        new MyThread().start();//0号线程(空参构造)
        new MyThread().start();//1号线程(空参构造)

       new YouThread().start();//2号线程(空参构造)
//如果是 new MyThread().run();
//     new YouThread().run();
//那就是先执行上面的全部,然后执行下面的全部
    }
}

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i < 1999; i++) {
            if (i % 20 == 0) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }else{
                try {//sleep方法必须要try-catch异常处理
                    sleep(1);//暂停线程1ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

class YouThread extends Thread{
    public void run() {
        for (int j = 1; j < 100; j++) {
            System.out.println(this.getName()+":匿名子类多线程测试");
            try {
                sleep(20);//暂停线程20ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

同一个MyThread类的对象m1只能调用一次start,若想再次调用必须再new一个m2,或者直接匿名对象调用。

 在线程类MyThread的定义中,this.相当于Thread.currentThread().

如果创建一个一次性线程,可以造一个匿名子类

new Thread(){
public void run(){
方法体内容
}
}.start();

 给线程命名:

setName()方法、构造器方法

注意:用构造器重命名需要在Thread子类MyThread中重写构造方法,因为构造方法不继承

class MyThread extends Thread {
    public MyThread(String str) {
        super(str);
    }

    public void run() {
MyThread m1 = new MyThread();
m1.setName("重命名xxx");
m1.start();
//先重命名后调用start

或者
MyThread m2 = new MyThread("重命名yyy");

即便是重命名了,每个创建的线程本身的编码private static int threadInitNumber;并不会改变,再次new一个线程,threadInitNumber仍然会在上一个基础上+1,并且与哪类Thread子类无关

例如:

public class ThreadTest {
    public static void main(String[] args) {
        new MyThread("线程一").start();//threadInitNumber=0
        new MyThread("线程二").start();//threadInitNumber=1
       new YouThread().start();//threadInitNumber=2
       new Thread(){//new一个匿名子类的匿名对象 threadInitNumber=3
          public void run(){
              for (int i=1;i<2000;i++){
                  System.out.println(this.getName()+"匿名子类重写run方法");
                  try {
                      sleep(10);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
       }.start();
    }
}

 对于主线程,原名main,更名方法如下

...main...{
MyThread m1 = new MyThread();
m1.setName("线程1");

Thread.currentThread().setName("主线程");
        for(int i= 1;i<2000;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }

}

运行结果

线程优先级

线程优先级有1~10十种等级,其中定义了三个静态常量,最小值,默认值,最大值

优先级可以通过setPriority()方法设置,其中

  • setPriority(10)相当于setPriority(MAX_PRIORITY)
  • setPriority(1)相当于setPriority(MIN_PRIORITY)
  • 不设置相当于setPriority(NORM_PRIORITY)
  •  设置完优先级,线程的执行仍然是多线程同时进行,只不过从概率角度讲,同一时间PRIORITY高的线程更容易执行,因此优先级高的线程更容易早执行完。
  • 优先级差别越大效果越明显,线程运行内容越多效果越明显(大数定律)
public class PriorityTest {
    public static void main(String[] args) {
        MyThread m1 = new MyThread("副线程1");
        m1.setPriority(Thread.MAX_PRIORITY);
        MyThread m2 = new MyThread("副线程2");
        m2.setPriority(Thread.MAX_PRIORITY);

        m1.setPriority(1);
        m2.setPriority(NORM_PRIORITY);
//也可以m2.setPriority(5);
//不设置优先级就默认m2.setPriority(NORM_PRIORITY);

        m2.start();
        m1.start();
    }

}

class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for(int i=1;i<100;i++){
            System.out.println(getName()+":"+i);
        }
    }
}

运行结果:可以看到优先级高的线程很快就执行完了

  多线程创建方式二:实现Runnable接口

  • 创建一个实现了Runnable接口的类,这个类可以再派生子类
  • 用这个实现类or其子类去实现Runnable中的抽象方法run()
  • new这个实现类的对象
  • 把这个对象作为参数传给Thread,创建Thread对象
  • 经过两次new,用最终的Thread对象调用start()
class MyrunThread implements Runnable {

    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class RunnableTest {
    public static void main(String[] args) {

        new Thread(new MyrunThread()).start();
//一步写法
        MyrunThread m1 = new MyrunThread();
        Thread M1 = new Thread(m1);
//两步写法
        M1.start();
    }
}

运行结果

两种方式的对比

1,开发中优先选择Runnable接口方式,原因如下

  • 该方法没有单继承的局限性
  • 适合处理多线程共享数据的情况

2,联系:public class Thread implements Runnable

3,相同点:

  • 都需要重写run()
  • 都要用最终对象调start()而不是调run()

4,区别:

  • 继承方式new一次,接口方式new两次

多线程创建方式三:有返回值:Callable接口

使用Runnable接口可以解决多线程的单继承问题,但是Runnable接口以及其Thread实现中的run方法都没有返回值,Callable接口解决了返回值问题

与Runnable相比,Callable功能更强大

java.lang.Runnable是JDK1.0产物

java.util.concurrent.Callable是JDK5.0产物

  • Runnable的run方法 相当于 Callablle的call方法(有返回值):   返回值由Future接口获取
  • call()方法可以抛异常:    run方法如果出现异常只能try-catch在内部处理,而call方法出现异常可以throws
  • call()方法支持泛型的返回值     :可选设置项,避免向下转型带来的安全隐患
  • 需要借助FutureTask类,比如获取返回结果

java.util.concurrent.Callable 下 Callable 接口的定义

@FunctionalInterface
public interface Callable{
    public V call() throws Exception;
}

Callable接口的使用

class Mythread implements Callable<泛型类X>{
public 返回值类型(类名X) call () throws 错误类型 {
    方法体
    return 能转成X的对象
}
}

public class Test{
public static void main(String[] args){

Mythread M1 = new Mythread();
FutureTask F1 = new FutureTask(M1);//和Runnable接口相比,多了这一步
Thread T1 = new Thread(F1,"线程名");

T1.start();//start运行call方法,但是不一定必须接收返回值
System.out.println(F1.get());


}

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

class Mythread implements Callable{

    @Override
    public Integer call() throws Exception {
int  sum = 0;
        for (int  i = 1 ; i<100 ; i++){
            if(i%4==0){
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
    }
}


public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
       Mythread M1 = new Mythread();//实现Callable接口实例
       FutureTask F1 = new FutureTask(M1);//传入FutureTask中
       Thread T1 = new Thread(F1);//传入Thread中
        T1.start();
        System.out.println(F1.get());

        //start运行call方法,但是不一定必须接收返回值
    }
}

多线程创建方式四:线程池

开发中一般都是用线程池

线程的生命周期

线程安全问题

如果创建的多个线程将对同一个共享数据进行操作,就可能导致读入和改写数据的各种错误。为了避免冲突,不同线程在对共享数据进行操作的时候应该有一个同步机制,甲访问该共享数据的时候应该对其上锁,乙丙丁等线程在上锁处停滞,等待甲操作完成再重复该步骤

线程同步synchronized

当多个线程同时操作同一资源时,对资源的保护正确读写的操作,有synchronized代码块、synchronized方法

以卖3个窗口100张票为例

synchronized同步代码块

该方法尽量使用Runnable接口方式来实现

格式:

synchronized(同步监视器){

  //同步监视器可以是任意类的对象,同一个对象同一把锁,处于同一个同步机制里

需要被同步的代码体

}

class MyThread implements Runnable {
    private static int ticket = 100;

    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
            if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }
            synchronized (this) {
                System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);

            }
        }
    }
}

public class SynchronizedTest {
    public static void main(String[] args) {
        Thread m = new Thread(new MyThread());
        m.start();
    }
}

如果改用extends方式来实现就会出现问题,因为synchronized同步代码块中的同步监视器(锁)是this,即为当前对象,而extends方法需要调用3个对象,所以上了3把锁,此时跟没上锁的效果是一样的,很容易就同一张票卖了几次

class MyThread extends Thread{
private static int ticket = 100;
public MyThread(String name){
    super(name);
}
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
            if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }
            synchronized (this) {
                System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);

            }
        }
    }
}

public class SynchronizedTest{
    public static void main(String[] args) {
        MyThread m1 = new MyThread("窗口1");
        m1.start();
        MyThread m2 = new MyThread("窗口2");
        m2.start();
        MyThread m3 = new MyThread("窗口3");
        m3.start();


    }
}

 

改进后的extends方式就是直接声明一个静态的任意对象当作同步监视器

private static Object obj = new Object();

class MyThread extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
public MyThread(String name){
    super(name);
}
    public void run() {

        while (true) {
            if(ticket<10) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
//            try {
//                sleep(10);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
            if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }
                System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);

            }
        }
    }
}

 此时的同步代码块包括了

synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
            if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }

如果把同步代码块缩小

  System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
            if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);

            }
        }

运行后

原因:3个线程只要有一个已经经过了if判断,但卡在了synchronized之前,那么他就会导致ticket=0变成ticket=-1,此后再也不会经过if语句来break了,所以停不下来

if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }

但如果同步代码块框的太多,跟单线程没区别

 因此,同步代码块既不能框的太少,也不能框的太多

synchronized同步方法

public 返回值 synchronized 方法(){ }

相当于

public 返回值 方法(){

synchronized(this){

....................

}  //同步方法相当于把整个方法用同步代码块框起来

}//并且以this为同步监视器

 一般都是定义一个synchronized方法,把这个方法放在run里

 效果和注意点跟同步代码块相同


public void run(){
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
show();
}

private synchronized void show(){
while(true){
if (ticket == 0) {
                System.out.println("已售罄");
                break;
            }
                System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);

            }
}
}

死锁

不同的线程分别需要占用对方需要的同步资源不放(不释放锁),都在等对方先放弃自己所需要的同步资源(锁),就形成的线程的死锁

出现死锁之后不会报错,也不会出现提示,所有线程都处于阻塞状态,无法继续

解决死锁的方法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

 一个线程run中A锁套B锁,另一个线程run中B锁套A锁,一旦两个同时运行就容易出现死锁。尤其是设置一个sleep沉睡,死锁更容易出现

class Mythread implements Runnable{
public void run(){
......
synchronized(锁A){
//A锁套B锁
    synchronized(锁B){
     ......
    }
}
......
}
}

class Youthread implements Runnable{
public void run(){
......
synchronized(锁B){
//B锁套A锁
    synchronized(锁A){
     ......
    }
}
......
}
}
//主方法体中
new Thread(new Mythread()).start();
new Thread(new Youthread()).start();

公平锁、非公平锁

jdk5之后提供了ReentrantLock类

ReentrantLock类的实例化有一个形参fair

private  ReentrantLock lockkk = new ReentrantLock(true);叫公平锁
private  ReentrantLock lockkk = new ReentrantLock();和
private  ReentrantLock lockkk = new ReentrantLock(false);叫非公平锁

公平锁:多个共用同一把锁的线程有序排队,并按排队顺序上锁

private  ReentrantLock lockkk = new ReentrantLock(true);//公平锁
。。。。。。。。。
    public void run(){
        for(int i = 1;i<=100;i++){
            lockkk.lock();//上公平锁lockkk
            System.out.print(Thread.currentThread().getName());
            Depot(this.account,1000);
            //输出内容
            try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }//加上sleep效果更明显,防止因为屏幕输出时间差造成的误导

            lockkk.unlock();//释放公平锁lockkk
//因为是公平锁,被阻塞的线程按顺序排队进入被lock和unlock方法夹在中间的代码块,
//此后该调用lock的线程进入排队的末尾,不与其他线程争抢资源
//配合上一个不短的sleep放大有序排队的效果,如果sleep值过小仍然可能冲突

        }
    }

非公平锁:多个共用同一把锁的线程有序排队,但不按顺序上锁,仍像synchronized一样靠抢

  private  ReentrantLock lockkk = new ReentrantLock(false);
  private  ReentrantLock lockkk = new ReentrantLock();
//两种表达都是表示非公平锁
    public void run(){
        for(int i = 1;i<=100;i++){
            lockkk.lock();

            try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }//sleep也可以放在输出语句之前,只要是在lock和unlock之间就行了
            System.out.print(Thread.currentThread().getName());
            Depot(this.account,1000);

            lockkk.unlock();

        }
    }

wait和notify、notifyAll

上面的ReentrantLock类提供的lock公平锁可以实现多个线程按顺序每个都执行一次,当

线程数=2时,wait notify方法也能达到这样的效果

private static Object obj = new Object();
 public void run(){
    synchronized(obj)
        for(int i = 1;i<=100;i++){
          obj.notify();//唤醒另一个线程
            try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
            System.out.print(Thread.currentThread().getName());
            Depot(this.account,1000);

        try {
                    obj.wait();//本线程阻塞,并释放同步监视器obj
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

        }
    }

当线程数>2时,即便是用notifyAll()也不能达到按顺序的效果,因为此时唤醒了其他线程之后,不像lock公平锁那样按顺序排队,notifyAll只管唤醒,线程资源的争夺仍然是随机的

  • wait():阻塞自己,释放锁(同步监视器)。wait之后仍然可以有语句
  • notify():唤醒一个线程,按优先级随机。一般放在代码块最开头
  • notifyAll():唤醒其他所有线程,不按顺序

wait()、notify()、notifyAll():三个方法都是

  • 写在同步方法or同步代码块中
  • 需要用同步监视器(锁对象)来调用
  • 因此定义在java.lang.Object中,因为需要所有对象都能调用
  • 报错信息是IllegalMonitorStateException

wait和sleep的区别

wait会释放同步监视器,其他线程可以进入synchronized代码块内部并获取锁

sleep不会释放同步监视器,如果是同步线程,那么sleep会导致整个多线程阻塞

wait定义在Object类中,需要在同步代码块中用同步监视器对象调用,sleep是定义在Thread类中的静态方法,随时可以调用

为什么同步代码块不能框多也不能框少

为什么synchronized同步代码块既不能框多,也不能框少_古细亚沾点-CSDN博客

线程通信:生产者消费者源码与解析

线程通信:生产者消费者源码与解析_古细亚沾点-CSDN博客

你可能感兴趣的:(java)