最近准备秋招,就开始之前java内容的复习,把复习过程中一些基本知识记录了下来,预祝自己秋招顺利吧。

基础篇

1.进程和线程

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

一个进程就包含了多个线程,每个线程负责一个单独的子任务。

进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

1.1进程和线程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

1.2上下文切换

上下文切换有时也叫做线程切换或者任务切换,是指cpu从一个进程(或线程)切换到另一个进程或(者线程)。上下文是指某一个时间点cpu寄存器和程序计数器的内容。

举例说明 线程A ---> B

1.先挂起线程A,将其在cpu中的状态保存在内存中。

2.在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。

3.当B执行完,根据程序计数器中指向的位置恢复线程A。


2.Java多线程入门类和接口

JDK提供了Thread类和Runnable接口来让我们实现自己的“线程”类。

  • 继承Thread类,并重写run方法;
  • 实现Runnable接口的run方法;

2..1 继承Thread

public class MyThread_01 extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread_01 run");
    }

    public static void main(String[] args) {
        MyThread_01 myThread_01 = new MyThread_01();
        myThread_01.start();
    }
}

注意要调用start()方法后,该线程才算启动!

我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。

注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出illegalThreadStateException异常。

2.2实现Runnable接口


小细节

当main线程启动一个线程的时候,main线程并不会阻塞,会和新开的线程交替执行

public class Demo1 extends Thread{
    @Override
    public void run() {
        for (int i=0; i<5;i++){
            try {
                Thread.sleep(500);
                System.out.println("线程启动了...【"+ Thread.currentThread().getName()+"】");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo1 demo1 = new Demo1();
        demo1.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("main 线程启动");
            Thread.sleep(1000);
        }
    }

}

运行结果:

main 线程启动
线程启动了...【Thread-0】
main 线程启动
线程启动了...【Thread-0】
线程启动了...【Thread-0】
main 线程启动
线程启动了...【Thread-0】
线程启动了...【Thread-0】
main 线程启动
main 线程启动

Process finished with exit code 0

实现Runnable接口的方式更加适合多个线程共享一个资源的情况


jdk1.8新特性Lamda表达式

属于函数式编程

lambda 表达式的语法格式如下:

(parameters) -> expression 

 (parameters) ->{ statements; }
public class MyLamda {
    public static void main(String[] args) {
        Like like = ()->{
            System.out.println("i like java lamda");
        };
        like.say();
    }
}

interface Like{
    void say();
}

线程的状态

image-20210714161604425
image-20210714190303037

线程的优先级

java中优先级可以指定,范围是1——10(从低到高),默认优先级为5

但是,并不一定是优先级越高越先执行,要取决于操作系统,java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级还是由操作系统决定的。

我们使用方法Thread类的setPriority()实例方法来设定线程的优先级。

守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

当主线程结束之后,守护线程也就结束

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

        Runnable runnable=()->{
            for( ; ; ){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我是守护线程");
            }
        };

        Thread thread = new Thread(runnable);
        thread.setDaemon(true);
        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("main线程");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程同步

在多线程编程,一些敏感数据不允许被多个线程同时访向,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访向,以保证数据的完整性

synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

死锁

产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

image-20210715145531281

释放锁

image-20210715145648921
image-20210715145957974

Lock锁

1、从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。

2、Lock 提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

3、Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

4、某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock 提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock 实现类。

5、Java8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLock。ReentrantReadWriteLock 为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。

public class MyLock {
    public static void main(String[] args) {
        TestLock testLock = new TestLock();
        new Thread(testLock).start();
        new Thread(testLock).start();
        new Thread(testLock).start();
    }
}

class TestLock implements Runnable{
    private int tickets = 100;
    //定义锁
    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            //加锁
            reentrantLock.lock();
            if(tickets<=0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+", "+(tickets--));
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //解锁
                reentrantLock.unlock();
            }
        }
    }
}