Java基础复习总结(day04)——15天

发布于:2022-10-18 ⋅ 阅读:(472) ⋅ 点赞:(0)

包装类

基本类型

对应的包装类(位于java.lang包中)

byte

Byte

short

Short

int

Integer

long

Long

float

Float

double

Double

char

Character

boolean

Boolean

包装类的三个常用用法:

字符串—>其他类型

Integer.toString();

Integer.parseInt();

Integer.valueOf();

public class Test {
    public static void main(String[] args) {
        int num = 123;
        String str = Integer.toString(num);
        double doublenum = Double.parseDouble(str);
        long longnum = Long.valueOf(str);
    }
}

线程

创建线程的三种方式

继承Thread

public class Main {
    //main方法是由主线程执行的,理解成main方法就是一个主线程。
    public static void main(String[] args) {
        //第三步,new一个MyThread线程类的对象
        Thread thread  = new MyThread();
        //第四部,运行start方法,最终还是执行run方法
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程"+i);
        }
    }
    //第一步,新建一个类继承自Thread
    static class MyThread extends Thread{
        //第二步,重写run()方法
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("zizizi子线程"+i);
            }
        }
    }
}

启动线程必须调用start()方法

多线程是并发抢占CPU执行,所以在执行的过程中会出现并发随机性。

Thread的常用API:

  1. public void setName(String name)给当前线程取名字
  2. public void getName()获取当前线程的名字,主线程默认main
  3. public static Thread currentThread()获取当前线程对象
  4. public static void sleep(long time)让线程休眠time毫秒

通过线程的有参构造取名字:

static class MyThread extends Thread{
    MyThread(String name){
        super(name);
    }
        //第二步,重写run()方法
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("子线程"+i);
            }
        }
    }

实现Runnable接口创建线程任务

Thread的构造器:

  1. Thread()
  2. Thread(String name)
  3. Thread(Runnable target)
  4. Thread(Runnable target,String name)
public class RunnableTest {
    public static void main(String[] args) {
        //第三步,new一个接口实现类的对象,target只是线程任务,并不是线程
        Runnable target = new MyThread();
        //第四步,new一个Thread线程
        Thread t1 = new Thread(target,"线程一");
        Thread t2 = new Thread(target,"线程二");
        t1.start();
        t2.start();
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
    //第一步,先继承Runnable接口
    static class MyThread implements Runnable{
        //第二步,重写run方法
    	@Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName() + i);
            }
        }
    }
}

实现Runnable接口来创建线程的优点:

  1. 只是实现了Runable的接口,还可以继承其他类。
  2. 同一个线程任务对象可以被包装成多个线程对象
  3. 适合多个线程去共享同一个资源
  4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码可以和线程独立
  5. 线程池可以放入实现Runnable或Callable线程任务对象。

注意:其实Thread类本身也是实现了Runnable接口的。

匿名创建方法:

Thread t = new Thread(new Runnable(){
    @Override
        public void run(){
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName() + i);
            }
        }
}).start();

实现Callable接口创建线程

public class CallableTest {
    public static void main(String[] args) {
        //第三步,new一个Callable接口实现类的对象
        Callable<String> callable = new MyThread();
        //第四步,把Callable任务对象包装成一个未来对象
        //未来任务对象其实是一个Runnable的对象,这样就可以被包装成线程对象。
        //未来任务对象在线程执行结束后去得到线程执行结果。
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        //第五步,新建线程
        Thread t = new Thread(futureTask);
        //启动线程
        t.start();
        //获取线程运行的结果
        try {
            String ans = futureTask.get();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
    //第一步,实现一个Callable接口类
    static class MyThread implements Callable<String>{
        //第二步,实现call方法
        public String call() throws Exception {
            int sum=0;
            for (int i = 0; i < 1000; i++) {
                System.out.println(Thread.currentThread().getName()+i);
                sum+=i;
            }
            return sum+"";
        }
    }
}

实现Callable接口来创建线程的优点:

  1. 只是实现了Callable的接口,还可以继承其他类。
  2. 同一个线程任务对象可以被包装成多个线程对象。
  3. 适合多个线程去共享同一个资源。
  4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码可以和线程独立。
  5. 很适合做线程池的执行任务。
  6. 可以直接获取线程执行结果。

线程安全问题

多个线程同时操作同一共享资源的时候可能会出现线程安全问题。

线程同步是用来解决线程安全问题的。

同步代码块

  • 同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
    需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

同步方法:

如果方法是实例方法:同步方法默认用this作为锁的对象。

如果方法是静态方法,默认用类名.class作为锁的对象。

Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。

线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

线程状态

导致状态发生条件

NEW(新建)

线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。

Runnable(可运行)

线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪

Blocked(锁阻塞)

当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

Waiting(无限等待)

一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

Timed Waiting(计时等待)

同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。

Teminated(被终止)

因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程池

就是容纳多个线程的容器,省去了频繁创建线程和销毁线程得操作。

线程池在Java中的代表类:ExecutorService(接口)

静态方法得到一个线程池的对象:

public static ExecutorService newFixedThreadPool(int nThreads);

ExecutorServoce提交线程任务对象执行的方法:

Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行

Future<?> submit(Callable task):提交一个Callable的任务对象给线程池执行

Runnable做线程池的任务

public class ExecutorTest {
    public static void main(String[] args) {
        //新建一个Runnable线程任务
        Runnable runnable = new Runnable() {
            public void run() {
                Thread.currentThread().setName("first");
                for (int i = 0; i < 1000; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        ExecutorService pools = Executors.newFixedThreadPool(3);
        
        pools.submit(runnable);
        pools.submit(runnable);
        pools.submit(runnable);
        pools.submit(runnable); //会先等待
        pools.shutdown();
    }
}

Callable做线程池的任务

public class ExecutorTest {
    public static void main(String[] args) {
        //新建一个线程池,核心任务数为3
        ExecutorService pools = Executors.newFixedThreadPool(3);
        //提交Callable的任务对象后返回一个未来任务对象
        Future<String> t1 = pools.submit(new MyThread(1000));
        Future<String> t2 = pools.submit(new MyThread(2000));
        Future<String> t3 = pools.submit(new MyThread(3000));
        //获取线程池执行的任务的结果
        try{
            System.out.println(t1.get());
            System.out.println(t2.get());
            System.out.println(t3.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    //Callable的实现类
    static class MyThread implements Callable<String> {
        private int num;
        MyThread(int num){
            this.num=num;
        }
        public String call() throws Exception {
            int sum = 0;
            for (int i = 0; i < num; i++) {
                sum+=i;
            }
            return Thread.currentThread().getName()+"结果为:"+sum;
        }
    }
}

线程池的作用

如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

死锁

死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
由于线程被无限期地阻塞,因此程序不可能正常终止。

java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,
便可让死锁消失

死锁一般存在资源的嵌套请求。

死锁案例

public class ThreadDead {
    public static Object resources1 = new Object();
    public static Object resources2 = new Object();

    public static void main(String[] args) {
        //线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resources1){
                    System.out.println("线程1已经占用资源1请求资源2");
                    try {
                        Thread.sleep(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (resources2){
                        System.out.println("线程1已经占用资源2");
                    }
                }
                }

        }).start();

        //线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resources2){
                    System.out.println("线程2已经占用了资源2,开始请求资源1");
                    try {
                        Thread.sleep(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (resources1){
                        System.out.println("线程2已经占用了资源1");
                    }
                }
            }
        }).start();
    }
}

volatile关键字

先来看一段代码案例:

public class VolatileThreadDemo {
    public static void main(String[] args) {
        VolatileThread volatileThread = new VolatileThread();
        volatileThread.start();
        while (true){
            if(volatileThread.isFlag()){
                System.out.println("执行~~"); //没有输出
            }
        }

    }
}
class VolatileThread extends Thread{
    private boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        this.flag = true;
        System.out.println("flag="+flag);
    }
}

程序中并不会输出“执行~”

不可见性的原因:每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值。

每个线程是在自己的工作内存中操作共享变量的。

解决方案:

并发编程下,多线程修改变量,会出现线程间变量的不可见性。

解决线程间变量的不可见性方案有两种常见方式:

1.加锁

每次加锁会清空线程自己的工作内存,从新读取内存最新值。

2.对共享的变量进行volatile关键字修饰。

volatile修饰的变量可以在多线程并发修改下,实现线程间变量的可见性。

一旦一个线程修改了volatile修饰的变量,另一个线程可以立即取到最新值。

加锁:

某一个线程进入synchronized代码块前后,执行过程入如下:

a.线程获得锁

b.清空工作内存

c.从主内存拷贝共享变量最新的值到工作内存成为副本

d.执行代码

e.将修改后的副本的值刷新回主内存中

f.线程释放锁

volatile:

  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存
  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存
  3. 此时main方法读取到了flag的值为false
  4. 当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本
  5. 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

原子性

在一次操作或者多次操作中,所有操作都得到了执行并且不受任何因素的影响,要么就不执行。

先看一段代码案例:

public class VolatileAtomicThread implements Runnable {
    private int cnt = 0;
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            cnt++;
            System.out.println("cnt的值为==》"+cnt);
        }
    }
}

class VolatileAtomicThreadDemo {
    public static void main(String[] args) {
        Runnable runnable = new VolatileAtomicThread();
        for (int i = 0; i < 100; i++) {
            new Thread(runnable).start();
        }
    }

}

cnt的值并不会每次都加到10000,没有保证原子性。

why?

因为每个线程在工作中是操作共享变量的。

用volatile修饰变量后有没有用?

没用,在volatile更新数据的过程中,可能已经进行了其他的操作,比如i++。

实现原子性的两种方式:

1、加锁 synchronized,性能差

2、使用原子类 AtomicInteger

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

原子型Integer,可以实现原子更新操作

public AtomicInteger():                 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer

int get():                               获取值
int getAndIncrement():                   以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():                   以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):                 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):                以原子方式设置为newValue的值,并返回旧值。
public class VolatileAtomicThread implements Runnable {
    // 原子类中封装好了整型变量,默认值是0
    private AtomicInteger atomicInteger = new AtomicInteger();
    @Override
    public void run() {
        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            // 底层变量+1且返回!
            System.out.println("count =========>>>> " + atomicInteger.incrementAndGet());
        }
    }
}

class VolatileAtomicThreadDemo {
    public static void main(String[] args) {
        // 创建VolatileAtomicThread对象
        Runnable target = new VolatileAtomicThread() ;
        // 开启100个线程对执行这一个任务。
        for(int x = 0 ; x < 100 ; x++) {
            new Thread(target).start();
        }
    }

}

为什么使用原子类可以保证原子性操作,且性能好,而且线程安全?

底层基于CAS乐观锁机制,每次修改数据不会加锁,等到修改的时候再判断有没有别人修改。

CAS

Compare And Swap(比较再交换)

是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。

在更新地址值之前,进程中的原先值先与地址值比较

CAS与Synchronized:乐观锁,悲观锁。

CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?

Synchronized是从悲观的角度出发(悲观锁)

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁

共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!

CAS是从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

本文含有隐藏内容,请 开通VIP 后查看