Java多线程与高并发一(基础知识)

        Java多线程是面试重要考点,其知识面涉及深度和广度都是其他面试题型所不及的,本博客系列《Java多线程与高并发》记录了博主学习高并发与多线程的路径,知识点由浅入深,并附有大量案例程序,可以作为笔记随时翻查。话不多讲,上干货。

程序、进程、线程

程序:是一个静态的实体,是一组有序指令的集合,就是躺在硬盘上的一堆代码文件

进程:程序运行起来,就是一个进程,每个进程占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等

线程:进程里面一个最小的执行单元,一个程序里不同的执行路径

Thread类

直接调用run是方法调用,调用start是启动一个线程

/**
 * thread 基础测试
 *
 * @author zab
 * @date 2019-10-25 22:32
 */
public class ThreadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"-----"+i);
        }
    }

    static class MyThread extends Thread{
        @Override
        public void run(){
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"-----"+i);
            }
        }
    }
}

上面的例子中,主线程和MyThread类开的线程会同时打印:线程名字+数字,调用的start方法,表示启动一个线程,启动的线程和主线程并行运行。结果如下:

Java多线程与高并发一(基础知识)_第1张图片

但是如果代码是调用的run方法:

/**
 * thread 基础测试
 *
 * @author zab
 * @date 2019-10-25 22:32
 */
public class ThreadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //注意这里如果直接调用run方法,则main方法中就只有一条执行路径
        myThread.run();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"-----"+i);
        }
    }

    static class MyThread extends Thread{
        @Override
        public void run(){
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"-----"+i);
            }
        }
    }
}

输出的结果就是这样的:

Java多线程与高并发一(基础知识)_第2张图片

可以看到,线程名称都是main,并且都是一个输出完99过后,才是另一个循环,而不是像start方法启动后,两个结果交替输出。

创建线程的两种方式

继承Thread,实现Runnable

/**
 * 怎样启动一个线程
 *
 * @author zab
 * @date 2019-10-25 22:48
 */
public class HowToStratThread {

    public void f(){
        System.out.println(Thread.currentThread().getName()+"启动了!");
    }

    public static void main(String[] args) {
        //方式一:继承Thread类,重写run方法
        new Thread1().start();
        //方式二:实现Runnable接口,重写run方法
        new Thread(new Thread2()).start();
        //方式三:匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"启动了!");
            }
        }).start();
        //方式四:lambda表达式
        new Thread(()->System.out.println(Thread.currentThread().getName()+"启动了!")).start();

        //方式五:lambda表达式的特殊写法
        HowToStratThread t = new HowToStratThread();
        new Thread(t::f).start();
    }

    static class Thread1 extends Thread{
        @Override
        public void run(){
            System.out.println(Thread.currentThread().getName()+"启动了!");
        }
    }
    static class Thread2 implements Runnable{
        @Override
        public void run(){
            System.out.println(Thread.currentThread().getName()+"启动了!");
        }
    }
}

可以看到效果:

Java多线程与高并发一(基础知识)_第3张图片

另外还有线程池ThreadPoolExcutor启动、实现Callable等启动方式,这些个高阶知识,我们后面再聊。

Thread 方法

  1. sleep(1000) 睡一秒
  2. yield()谦让进入等待队列中,让一下CPU
  3. join() 线程t1中如果调用t2的join()方法,t1线程会等t2执行完毕才继续往下执行
  4. stop() 废弃的终止程序线程方法,太粗暴
  5. 其他不太重要的方法,设置守护线程,获取线程名称,设置优先级(太鸡肋)等等
/**
 * thread 方法测试
 *
 * @author zab
 * @date 2019-10-25 22:59
 */
public class ThreadMethodTest {

    static Thread2 thread2 = new Thread2();

    public static void main(String[] args) {
        new Thread(new Thread1()).start();
        thread2.start();
    }

    static class Thread1 implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "正在执行开始!!!!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在执行结束!!!!");
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "正在执行开始!");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在执行结束!");
        }
    }
}

上面案例简单测试了下join方法,可以看到输出结果是:虽然Thread1只睡了一秒,但是也是等睡了3秒的Thread2执行完了,Thread1才继续往下执行的。

Java多线程与高并发一(基础知识)_第4张图片

线程状态

Java多线程与高并发一(基础知识)_第5张图片

 

线程状态:当线程类创建对象后,就是new状态,调用方法start()后,就是Runnable状态,可运行态分Ready和Running,正在运行的线程调用yield方法让出cpu,就会从Running转为Ready,其他一些导致线程等待或者阻塞的情况参见上图。

getState()可获取线程状态

synchronized关键字

加在方法上和锁代码块

public synchronized void f(){
    //代码逻辑
}
public void f(){
    //代码逻辑
    synchronized(o){
        //代码逻辑
    } 
    //代码逻辑
}

 o不要用String、Integer、Long等基础的对象,因为共享原因可能锁到别人用的值。

锁,锁的是某个对象,拿到某个对象的锁才能执行大括号的代码。

synchronized方法和synchronized(this)等价

同步方法可以调用非同步方法,即synchronized方法中,可以调用非synchronized方法。

我们来看看多线程的情况下,没有同步方法会有什么问题:

import java.util.concurrent.Semaphore;

/**
 * synchronized测试
 *
 * @author zab
 * @date 2019-10-25 23:21
 */
public class SyncTest {
    static Semaphore semaphore1 = new Semaphore(0);
    static Semaphore semaphore2 = new Semaphore(0);
    int i = 0;
    public void f1(){
        for (int j = 0; j < 100000; j++) {
            i++;
        }
        semaphore1.release();
    }
    public void f2(){
        for (int j = 0; j < 100000; j++) {
            i++;
        }
        semaphore2.release();
    }

    public static void main(String[] args) {

        SyncTest t = new SyncTest();
        new Thread(t::f1).start();
        new Thread(t::f2).start();
        try {
            semaphore1.acquire();
            semaphore2.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.i);
    }
}

案例中,每个线程都对SyncTest的公有变量i做十万次的自增操作,但是多次结果输出来却不是二十万。Semaphore是信号量,其中acquire方法表示需要一个许可才能继续往下,release方法则是给当前Semaphore对象释放一个许可。这样保证了只有当两个线程都执行完了,才会打印i的值。

Java多线程与高并发一(基础知识)_第6张图片

Java多线程与高并发一(基础知识)_第7张图片

那如果加上synchronized关键字呢???

import java.util.concurrent.Semaphore;

/**
 * synchronized测试
 *
 * @author zab
 * @date 2019-10-25 23:21
 */
public class SyncTest {
    static Semaphore semaphore1 = new Semaphore(0);
    static Semaphore semaphore2 = new Semaphore(0);
    int i = 0;
    public synchronized void f1(){
        for (int j = 0; j < 100000; j++) {
            i++;
        }
        semaphore1.release();
    }
    public synchronized void f2(){
        for (int j = 0; j < 100000; j++) {
            i++;
        }
        semaphore2.release();
    }

    public static void main(String[] args) {

        SyncTest t = new SyncTest();
        new Thread(t::f1).start();
        new Thread(t::f2).start();
        try {
            semaphore1.acquire();
            semaphore2.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.i);
    }
}

 

Java多线程与高并发一(基础知识)_第8张图片

为了避免作弊,截图比较全,可以看到真的是二十万。

可重入锁

假如m1是同步的(被synchronized修饰),m2也是同步的

在m1中调用m2,是允许的。这种现象叫锁的可重入,synchronized是可重入锁。

为什么必须是可重入锁?设想父类有个synchronized方法,如果不能重入,子类方法想要继承父类方法是有问题的。

public class Father {
    protected synchronized void f() {
        System.out.println("我是父亲f方法");
    }

    public static void main(String[] args) {
        Son son = new Son();
        son.f();
    }
}

class Son extends Father{
    @Override
    public synchronized void f(){
        super.f();
        System.out.println("我是儿子f方法");
    }
}

程序输出:

我是父亲f方法
我是儿子f方法

Process finished with exit code 0

而实验证明,在子类的synchronized方法中是可以调用父类的synchronized方法的。也就证明了synchronized锁的可重入性。

程序出异常,synchronized锁会被释放

synchronized锁升级

JDK早期1.4之前版本,synchronized是重量级的,每用一次都会向操作系统申请锁。

1.6以上的synchronized锁,效率大大提升,得益于锁的升级过程。

无锁:最开始没有线程访问时,synchronized修饰的方法或者代码块是没有锁的。

偏向锁:一个对象刚开始实例化的时候,没有任何线程来访问它的时候,是可偏向的,意味着它现在认为只可能有一个线程来访问它。所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头MarkWord成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作(高效的原因)。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,操作系统检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是这个时候升级为自旋锁)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

自旋锁:如果有多个线程争用,自旋10次,看是否能够获得锁,操作系统用户态解决问题,不是内核态,也叫轻量级锁

重量级锁:向操作系统申请,线程阻塞等待

这个是面试常问的,锁的四种状态??

执行时间短,线程数比较少,自旋锁适合

执行时间长,线程数比较多,重量级锁适合

你可能感兴趣的:(Java并发编程)