线程的安全性

线程

1 进程

  • 概念:
    程序是静态的形式,而进程就是程序运行时的实体状态,在CPU执行时,才被赋予生命。
    CPU调度进程的方式:在单核CPU下任何时间点只能有一个进程被执行,CPU将一段时间分成若干时间片,为每个进程分配时间片,获得时间片的进程才可以执行,其他进程进入等待(挂起)宏观并行,微观串行。
    

2 线程

  • 概念:线程是轻量级的进程,一个进程中包含了若干线程,在Java中没有多进程,因为一个JVM就是一个进程,CPU调度时线程是最小的调度单位。
  • 线程的重要组成部分:
    1. OS(操作系统):分配时间片。
    2. 数据:堆空间(对象)共享,栈空间(局部变量)独立。
    3. 线程任务:代码。
    

3 创建线程为线程分配任务

3.1 继承Thread类,覆盖run方法
  • 演示的代码如下:
    class MyThread extends Thread{
        @Override
        public void run(){						// 将线程要运行的代码定义在run方法中
            
        }
    }
    
3.2 实现Runnable接口
  • 演示的代码如下:
    class MyTask implements Runnable{
        @Override
        public void run() {
           // 将线程要运行的代码定义在run方法中
        }
    }
    // 1.创建任务对象Runanble
    Runnable run = new MyTask();
    // 2.创建线程对象分配任务
    Thread t1 = new Thread( run );
    t1.start();			// 启动新线程自动执行任务
    
3.3 常用方法
  • 方法名作用
    start()启动新线程自动调用run方法执行任务
    join()其他线程加入到本线程,待其他线程执行完毕后执行本线程
    sleep(millis)线程等待若干毫秒

演示的代码如下:

package com.txw.test;

import java.util.Scanner;
public class TestThread {
    public static void main(String[] args) throws Exception{
        System.out.println("Main----start");
        // Thread t1 = new MyThread();		// 创建线程对象
        // t1.run(); 不要手动调用run方法,否则不会启动新线程执行该方法
        // t1.start();		// 启动新线程并自动调用run方法
        // 1.创建任务对象Runanble
        Runnable run = new MyTask();
        // 2.创建线程对象分配任务
        Thread t1 = new Thread( run );
        t1.start();		// 启动新线程自动执行任务
        Scanner sc = new Scanner(System.in);
        for (int i = 0; i < 3000; i++) {
            System.out.println("MainThread "+i);
            Thread.sleep(300);// 休眠300毫秒
        }
        System.out.println("Main---end");
    }
}
// 线程类
class MyThread extends Thread{
    @Override
    public void run(){		// 将线程要运行的代码定义在run方法中
        System.out.println("t1---start");
        for (int i = 0; i < 3000; i++) {
            System.out.println("MyThread.run "+i);
        }
        System.out.println("t1---end");
    }
}
// 任务类
class MyTask implements Runnable{
    @Override
    public void run() {
        System.out.println("t1---start");
        for (int i = 0; i < 3000; i++) {
            System.out.println("MyThread "+i);
        }
        System.out.println("t1---end");
    }
}

4 线程同步

  • 相关概念:
    1. 异步(assynchronous):多线程之间不会相互等待、阻塞,各行其是,各安天命互不干涉,是线程的默认执行方式。
    2. 同步(synchronized):将多线程的并行(异步执行)转换为串行(同步执行),如某个线程的任务没有执行完毕,其他线程会进入阻塞,是单线程的默认执行方式。
    3. 临界资源:在多线程环境下共享的资源(对象)
    4. 原子操作:由多个步骤构成的功能,每个步骤不可被分割不可被中断。
    5. 线程不安全:当多线程并发访问临界资源时破坏原子操作,造成的数据不一致问题。
    6. 对象互斥锁:是每个对象都拥有的一个锁标记一个对象只有一个,得到锁标记的线程才能够进行相应的操作,操作完毕归还锁标记。
    7. 对象锁池:用于存储等待对象锁标记的线程。
    
    演示的代码如下:
package com.txw.test;

public class TestAccount {
    public static void main(String[] args) {
        Account acc = new Account(50000D);
        Thread husband = new Husband( acc );		// 丈夫线程
        Thread wife = new Wife( acc );		// 妻子线程
        husband.start();
        wife.start();
    }
}
// 丈夫(线程) 阿森
class Husband extends Thread{
    Account acc;		// 临界资源
    public Husband(Account acc) {
        this.acc = acc;
    }
    public void run() {
        // 竞争锁标记
       // synchronized ( acc ) {
            // 从账户中取款(原子操作)
            acc.withdraw(15D);
        //}
        // 释放锁标记
    }
}

// 妻子(线程)腾腾
class Wife extends Thread{
    Account acc;		// 临界资源
    public Wife(Account acc) {
        this.acc = acc;
    }
    public void run() {
        // 竞争锁标记
        // synchronized ( acc ) {
            // 从账户中取款(原子操作)
            acc.withdraw(50000D);
        // }
        // 释放锁标记
    }
}
class Account{
    Double balance;
    public Account() { }
    public Account(Double balance) {
        this.balance = balance;
    }// 取款方法
    // Husband acc.withdraw() ---> this:acc
    // Wife    acc.withdraw() ---> this:acc
    // this: acc
    public synchronized void withdraw(Double money){
        System.out.println(this);
        // synchronized ( this ) {
            if (money <= balance) {
                System.out.println(Thread.currentThread().getName() + "正在取款....");
                System.out.println(Thread.currentThread().getName() + "当前账户余额为:" + balance);
                System.out.println(Thread.currentThread().getName() + "取款金额为:" + money);
                balance -= money; // 扣除余额
                System.out.println(Thread.currentThread().getName() + "取款成功,当前账户余额为:" + balance);
            } else {
                System.out.println("余额不足!!!");
            }
        // }
    }
}
4.1 同步方式1 synchronized代码框
  • 语法如下:
    synchronized( 临界资源| 共享对象 ){
        // 原子操作
    }
    
    当线程试图进入到synchronized代码块时先竞争临界资源锁标记(把对象锁住把钥匙拿走),只有得到锁标记的线程才能执行{}中的代码,其他线程进入对象锁池,当得到锁标记的线程完成原子操作(出synchronized代码块)其他线程再次竞争。
4.2 同步方式2 synchronized方法
  • 语法如下:
    访问权限修饰符 synchronized 返回值类型 方法名(参数表)异常{
        
    }
    // 等同于
    访问权限修饰符  返回值类型 方法名(参数表)异常{
        synchronized( this ){
            
        }
    }
    
    演示的代码如下:
package com.txw.test;

public class TestDeadLock {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(){
            public void run(){
                synchronized ( lock1 ){
                    System.out.println("t1-----lock1");
                    try{lock1.wait();}catch(Exception e){}		// 释放锁标记
                    synchronized ( lock2 ){
                        System.out.println("t1-----lock2");
                    }
                }
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                synchronized ( lock2 ){
                    System.out.println("t2-----lock2");
                    synchronized ( lock1 ){
                        System.out.println("t2-----lock1");
                        lock1.notify();		// lock1.notifyAll(); 唤醒因调用lock1.wait()方法进入等待的线程
                    }
                }
            }
        };
        t1.start();
        t2.start();
    }
}
4.3 线程状态图

在这里插入图片描述

5 习题

  1. 关于线程设计,下列描述正确的是(C )
    A. 线程对象必须实现 Runnable 接口
    B. 启动一个线程直接调用线程对象的 run()方法
    C. Java 对多线程同步提供语言级的支持
    D. 一个线程可以包括多个进程
    原因:
    A错误,写一个多线程程序还可以继承Thread
    B错误,启动一个线程调用start()方法由虚拟机调用run()方法
    D错误,一个进程包括多个线程
  2. 在 java 中的线程模型包含(ABC)
    A. 一个虚拟处理器(CPU)
    B. 执行的代码
    C. 代码操作的数据
    D. 以上都不是
  3. 有关线程,那些描述是正确的?(B)
    A. 一旦一个线程被创建,它就立即开始运行
    B. 使用 start()方法可以是一个线程成为可运行的,但它不一定立即开始运行
    C. 最多只能有一个线程处于就绪状态
    D. 调用 sleep 方法可以让当前线程终止
    原因:
    A错误,线程被创建需要调用start方法让线程处于就绪状态,抢到cpu的执行权才能执行。
    C错误,可以有多个线程处于就绪状态,哪个线程抢到执行权就可以执行代码
    D错误,调用sleep方法是让当前线程进入有限期的休眠状态,时间到就会醒来回到就绪状态。
  4. Runnable 接口中定义了如下的哪些方法(B)
    A. start()
    B. run()
    C. stop()
    D. sleep()
  5. Thread 类位于以下哪一个包中(A)
    A. java.lang
    B. java.util
    C. java.math
    D. java.util.concurrent
  6. 以下哪个方法是用于定于一个线程的执行体(C)
    A. start()
    B. main()
    C. run()
    D. init()
  7. 线程调用 sleep 方法后,线程会进入的状态是(C)
    A. 就绪状态
    B. 可运行状态
    C. 有限期等待状态
    D. 终止状态
  8. 下面关于 synchronized 关键字描述正确的是(B)
    A. 允许两个线程并行运行
    B. 保证在某一个时刻只有一个线程可以访问方法或是对象
    C. 保证两个或是更多的线程同时开始和结束
    D. 以上描述都不正确
  9. 以下哪些情况可以终止当前线程的运行(A)
    A. 抛出一个异常时 B. 当该线程调用 sleep 方法时
    C. 当创建一个新的线程时
    D. 当线程调用 join 方法时
    原因:
    B错误调用sleep只是有限期的等待,时间到如果抢到cpu的执行权, 该线程会继续执行。
    C错误,一个进程可以有多个线程,创建一个新线程,不做任何操作, 不会影响其他线程的正常结束
    D错误,join方法是等待当前线程结束,并不会终止。
  10. 关于线程,判断以下描述的是否正确。
    (1) Java 线程有多种不同的状态,这几种状态中的任何两种状态之间都可以相互转换。(F)
    (2) 在程序中,不可分割的多步操作通常被称为原子操作,一旦被破坏,可能会影响数据的正确性(T)
    (3) 线程的启动可以执行调用 run 方法。(F)
    (4) 当线程所定义的 run 方法执行完毕时,线程就会终止。(T)
    (5) 使用 Thread 子类创建线程的优点是可以在子类中增加新的成员使线程具有某种属性,也可以在子 类中新增加方法,使线程具有某种功能;但是 Java 不支持多继承,Thread 类的子类不能再扩展其他 的类。(T)
  11. 仔细阅读以下代码,关于程序描述正确的是(C )
    在这里插入图片描述
    A. 代码编译失败,因为 ex2.run()无法获得执行
    B. 代码编译成功,存在 3 个可运行的线程
    C. 代码编译成功,存在 1 个可运行的线程
    D. 以上描述都不正确
    原因:多个线程会抢夺cpu的执行权,哪个线程抢到就执行,在一个时间点cpu 只能执行一个线程。
  12. 仔细阅读以下代码,关于程序描述正确的是(C)
    在这里插入图片描述
    A. 打印输出,从 0 至 limit
    B. 无内容输出,因为没有明确调用 run()方法
    C. 代码编译失败,因为没有正确实现 Runnable 接口
    D. 代码编译失败,如果声明类为抽象类,可使代码编译成功
    E. 代码编译失败,如果去掉 implements Runnable,可使代码编译成功。
    原因:没有正确覆盖run()方法。
  13. 自定义异常类时,可以继承的类是(A)。
    在这里插入图片描述
    A. 编译出错
    B. 运行时异常
    C. 正常编译运行,输出 sleep
    D. 正常编译运行,但没有内容输出
    原因:Sleep方法有异常需要处理。
  14. 仔细阅读以下程序,在//1 和//2 处加上的 synchronized 起什么作用?如果不加 synchronized,运 行程序有什么不同的地方?
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    答:
    加上 synchronized 表示对 lock 对象加锁,这样能保证输出 10$$$之后再输出 10 个###。如果不加 上 synchronized 的话,则程 序运行时输出的顺序每次执行都不同。
  15. 仔细阅读以下代码,关于程序结果的输出描述正确的是(D)
    在这里插入图片描述
    A. 先输出 100 个 hello,然后是 100 个 world
    B. 先输出 100 个 world,然后是 100 个 hello
    C. 线程不同步,因此交替输出 hello 和 world
    D. 以上说法都不正确
    原因:两个线程都没有为data赋值。
  16. 仔细阅读以下代码,希望能够同步的输出 aaa 和 bbb,即一次输出 100 个 aaa 或 bbb,输出这两个 字符串时没有交互,为达到上述目的,要对原代码进行修改。以下哪些修改方式能够得到想要的结果(C)
    在这里插入图片描述
    A. 把第 6 行改为 public synchronized void run()
    B. 把 run 方法中所有的内容都放在 synchronized(data)代码块中
    C. 把 run 方法中所有的内容都放在 synchronized(System.out)代码块中
    D. 以上做法都不能达到要求
  17. 仔细阅读以下代码,将程序中不正确的地方进行改正。
    在这里插入图片描述
    答:修改的代码如下:
package com.txw.test;

class MyThread1 implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            // sleep()方法有异常需要处理 
            try {
                Thread.sleep((int) (Math.random() * 1000));    // 该类实现接口接口,sleep方法是 Thread类中的一个静态方法,应该使用类名调用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello");
        }
    }
}
class MyThread2 extends Thread {
    public void run() {		 // run 方法不能抛出任何异常 
        for (int i = 0; i < 100; i++) { 			// 需要对异常进行处理
            try {
                this.sleep((int) (Math.random() * 1000));
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block 
                e.printStackTrace();
            }
            System.out.println("world");
        }
    }
}
public class Test{
    public static void main(String args[]) {
        Runnable t1 = new MyThread1();
        Thread t2 = new MyThread2();
        // t1.start(); // 不能对 t1 调用 start 方法,应该利用 t1 创建一个 Thread 对象
        Thread t = new Thread(t1);
        t.start(); t2.start(); 
    } 
}
  1. 编程:创建两个线程,要求如下:
    (1) 一个线程输出 1~26,另一个线程输出 A~Z。
    (2) 一个线程使用继承 Thread 类的写法,另一个线程使用实现 Runnable 接口的写法。
    演示的代码如下:
package com.txw.test;

public class TestThread {
    public static void main(String[] args) {
        // 创建两个线程 
        Thread t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();
        Thread t3 = new Thread(t2);
        t1.start();
        t3.start();
    }
}
// 任务打印1~26
class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 1; i < 27; i++) {
            System.out.println(i);
        }
    }
}
// 任务打印A~Z 
class MyThread2 implements Runnable{
    @Override
    public void run() {
        for(char i = 'A';i<='Z';i++){
            System.out.println(i);
        }
    }
}
  1. 编程:假设一个银行的 ATM 机,它允许用户存款也可以取款。现在一个账户(Account)上存款为 200 元,用户 A 和用户 B 都拥有这个账户存款和取款的权利。用户 A 存入 100 元,而用户 B 将取出 50 元, 则最终账户的存款应为 250 元。为了模拟以上过程,描述如下:
    (1) 定义一个 Account 类,属性为账户名(userName)和余额(balance),类进行封装。
    (2) 线程 ThreadA 负责存钱操作;线程 ThreadB 负责取钱操作;
    (3) main 函数中负责开启两个线程,并将存取操作结束之后的账户余额进行打印输出。
    演示的代码如下:
package com.txw.test;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account("张三",200);
        ThreadA t1 = new ThreadA(a);
        ThreadB t2 = new ThreadB(a);
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        System.out.println(a.getBalance());
    }
}
// 账户A---》存钱 
class ThreadA extends Thread{
    private Account a;

    public ThreadA(){
    }

    public ThreadA(Account a){
        this.a=a;
    }

    public void run(){
        synchronized (a) {
            // System.out.println("存储钱了.....+100");
            a.setBalance(a.getBalance()+100);
            // System.out.println("余额:"+a.getBalance());
        }
    }
}
// 账户B---》取钱 
class ThreadB extends Thread{
    private Account a;

    public ThreadB(){

    }
    public ThreadB(Account a){
        this.a=a;
    }

    public void run(){
        synchronized (a) {
            if(a.getBalance()>=50){
                double s=a.getBalance()-50;
                a.setBalance(s);
                // System.out.println("取钱了....."+50);
            }
        }
    }
}
class Account{
    private String name;
    private double balance;

    public Account() {
        super();
    }

    public Account(String name, double balance) {
        super();
        this.name = name;
        this.balance = balance;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "name=" + name + ", balance=" + balance ;
    }
}
  1. 简述 ArrayList 和 CopyOnWriteArrayList 的区别。
    答:
    ArrayList:线程不安全,效率高。
    CopyOnWriteArrayList:线程安全,效率比Vector要高。实现原理是读操作不加锁,写操作是复制一份在新的数组 做写操作。
  2. 简述 HashMap 和 ConcurrentHashMap 的区别。
    答:
    HashMap:线程不安全,效率高。
    ConcurrentHasHMap:线程安全,效率比Hashtable高,底层使用了分段锁。
  3. 简述线程池的好处及获取线程池对象的方式。
    答:
    线程池的好处:
    提高效率,创建好一定数量的线程放在池中,等需要使用的时候就 从池中拿一个,这要比需要的时候创建一个线程对 象要快的多。
    创建线程池对象的方式:
    方式1:创建固定大小的线程池。 Executors.newFixedThreadPool(2); // 2个
    方式2:动态增加线程池。 Executors.newCachedThreadPool();
    方式3:创建一个单线程的线程池 Executors.newSingleThreadExecutor();
  4. 编程:利用线程池完成以下线程任务:
    (1) 一个线程任务打印输出 100 个”&&&&&&”; 一个线程任务打印输出 100 个”***********”。
    (2) 完成以上两个线程任务之后,继续完成一个线程任务打印输出 100 个”~~~~~~~~~~”;一个 线层任务打印输出 100 个”#############”
    演示的代码如下:
package com.txw.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
    public static void main(String[] args) {//创建一个线程池,最初创建连个线程
        ExecutorService es = Executors.newFixedThreadPool(2);
        //往线程池中提交任务,第一个任务打印10个&&&&& 
        es.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("&&&&&");
                }
            }
        });
        //往线程池中提交任务,第一个任务打印10个***** 
        es.submit(new Runnable() {
            @Override public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("*****");
                }
            }
        });
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block 
            e.printStackTrace();
        }
        // 往线程池中提交任务,第一个任务打印10个~~~~~
        es.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("~~~~~");
                }
            }
        });
        //往线程池中提交任务,第一个任务打印10个####
        es.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("####");
                }
            }
        });
        es.shutdown();
    }
}
  1. 编程:利用线程池及相关技术完成以下要求:
    (1) 一个线程任务是将 26 个大写字母(A~Z)拼接为一个字符串 。
    (2) 另一个线程任务是将 26 个小写字母(a~z)拼接为一个字符串。
    (3) 在主线程中分别获取两个线程的拼接结果,将结果按照 AaBbCc…的属性进行重组为新的字符串, 并打印输出。
    演示的代码如下:
package com.txw.test;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test{
    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newFixedThreadPool(2);
        Future<String> f1 = es.submit(new A());
        // 任务2将A~Z每一个字符拼接在一起
        Future<String> f2 = es.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                String s = "";
                for(char ch = 'A'; ch<='Z'; ch++){
                    s += ch;
                }
                return s;
            }
        });
        // 两个线程结束后分别得到a~z A~Z 两个字符串 
        String az = f1.get();
        String AZ = f2.get();
        es.shutdown();
        String s = "";
        // 两个字符串交替拼接 AaBbCc 
        for(int i = 0; i<26; i++){
            s += AZ.charAt(i);
            s += az.charAt(i);
        }
        System.out.println(s);
    }
}
// 任务1将a~z字符拼接在一起
class A implements Callable<String>{
    @Override
    public String call() throws Exception {
        String s = "";
        for(char ch = 'a'; ch<= 'z'; ch++){
            s += ch;
        }
        return s;
    }
}

6 总结

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学无止路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值