10.从零开始学习Java-多线程

多线程

首先线程是CPU的基本调度单位,

然后两种创建线程的方法(其中,方法2继承Runnable最为常用),

然后加锁两种解决线程不同步的方法。

有了锁就有可能产生死锁的问题,(线程之间都需要对方的锁标记,同时自己不会释放锁,产生死锁)

通过线程通信(wait和notify)解决死锁。

一、什么是多线程

1.1 进程:

程序是静止的,只有真正运行时的程序, 才被称为进程。

单核CPU在任何时间点上, 只能运行一个进程;

宏观并行、微观串行。//自己的理解是线程串行。

1.2 线程:

线程,又称轻量级进程(Light Weight Process)。 程序中的一个顺序控制流程,同时也是CPU的基本调度单位。

1.3 多线程:

进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程。

例如:迅雷是一个进程,当中的多个下载任务即是多个线程。

Java虚拟机是一个进程,当中默认包含主线程(Main), 可通过代码创建多个独立线程,与Main并发执行。

1.4 进程和线程的区别:

  • 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
  • 一个程序运行后至少有一个进程。
  • 一个进程可以包含多个线程,但是至少需要有一个线程。
  • 进程间不能共享数据段地址,但同进程的线程之间可以。

二、线程的组成

2.1 组成:

  • 任何一个线程都具有基本的组成部分:
    • CPU时间片:操作系统(OS)会为每个线程分配执行时间。
    • 运行数据://堆空间共享,栈空间独立。
      • 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
      • 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
    • 线程的逻辑代码。

2.2 创建线程(1):

1.继承Thread类

2.覆盖run()方法

3.创建子类对象

4.调用start()方法

在这里插入图片描述

2.3 创建线程(2):

1.实现Runnable接口

2.覆盖run()方法

3.创建实现类对象

4 .创建线程对象

5.调用start()方法

//更为常用,也更为方便。

在这里插入图片描述

三、线程的状态

3.1 线程的状态(基本):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-brR3Ztsk-1600415585994)(C:\Users\Administrator\Desktop\千峰笔记\线程的状态.png)]
在这里插入图片描述

1.New 初始状态

线程对象被创建,即为初始状态。 只在堆中开辟内存,与常规对象无异。

2.Ready 就绪状态

调用start()之后,进入就绪状态。 等待OS选中,并分配时间片。

3.Running 运行状态

获得时间片之后,进入运行状态, 如果时间片到期,则回到就绪状态。

4.Terminated 终止状态

主线程main()或独立线程run()结束, 进入终止状态,并释放持有的时间片。

3.2 常见方法:

//都有暂停。

  • 休眠://等待的是时间
    • public static void sleep(long millis)
    • 当前线程主动休眠 millis 毫秒。
  • 放弃:
    • public static void yield()
    • 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
  • 结合://等待触发条件
    • public final void join()
    • 允许其他线程加入到当前线程中,并优先执行。
public class TestImplementsRunnable {

	public static void main(String[] args) throws InterruptedException {

		System.out.println("程序开始");
		
		Thread.sleep(5000);
		
		MyRunnable task = new MyRunnable();//任务Task(输出50次)
		
		Thread t1 = new Thread(task);
		
		Thread t2 = new Thread(task);
		
		t1.start();
		t2.start();
		
		System.out.println("程序结束");
	}
}

class MyRunnable implements Runnable{
	@Override
	public void run() {//任务
		for (int i = 1; i <= 50; i++) {
			System.out.println( Thread.currentThread().getName() + " : " + i);
//			try {
//				Thread.sleep(1000);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
		}
	}
}
package methods;

public class TestThreadMethods {
	public static void main(String[] args) throws InterruptedException {

		Thread t1 = new Thread(new Task1());
		
		Thread t2 = new Thread(new Task2());
		
		//t1.start();
		
		t2.start();		
		
		for (int i = 1; i <= 1000; i++) {
			System.out.println("main:" + i);
			if(i == 200){
				t2.join();//将t2加入到我main线程中,等待t2线程的结束后,再执行。
			}
		}
	}
}

class Task1 implements Runnable{
	@Override
	public void run() {
		for (int i = 1; i <= 1000; i++) {
			System.out.println("Task1:" + i);
			if(i % 10 == 0){
				Thread.yield();//主动放弃时间片
			}
		}
	}
}

class Task2 implements Runnable{
	@Override
	public void run() {
		for (int i = 1; i <= 1000; i++) {
			System.out.println("---Task2:" + i);
		}
	}
}

3.3 线程的状态(等待):

在这里插入图片描述

3.4 线程状态

在这里插入图片描述

四、线程安全

在这里插入图片描述

  • 需求:A线程将“Hello”存入数组的第一个空位;B线程将“World”存入数组的第一个空位。
  • 线程不安全:
    • 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
    • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
    • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

//一般来说,增删改需要加锁(线程安全),查不需要加锁(速度会快一些)。

4.1 线程同步(1):

同步代码块:

synchronized(临界资源对象){ 

	//对临界资源对象加锁 //代码(原子操作)

 }

注:

  • 每个对象都有一个互斥锁标记,用来分配给线程的。
  • 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
  • 线程退出同步代码块时,会释放相应的互斥锁标记。

线程的状态(阻塞):

在这里插入图片描述

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

		// 按照该卡号进行了开户,并存入了2000元
		Account acc = new Account("1001", "123456", 2000D);// 0x11223344

		Husband h = new Husband(acc);

		Wife w = new Wife(acc);
		
		Son s = new Son(acc);

		Thread t1 = new Thread(h);

		Thread t2 = new Thread(w);
		
		Thread t3 = new Thread(s);

		t1.start();// Husband
		t2.start();// Wife
		t3.start();// Son

	}
}

class Husband implements Runnable {
	Account acc;
	public Husband(Account acc) {
		this.acc = acc;
	}

	@Override
	public void run() {
		// synchronized(acc){//对acc对象加锁
		this.acc.withdrawal("1001", "123456", 1200D);
		// }
	}

}

class Wife implements Runnable {
	Account acc;
	public Wife(Account acc) {
		this.acc = acc;
	}

	@Override
	public void run() {
		// synchronized(acc){
		this.acc.withdrawal("1001", "123456", 1200D);
		// }
	}
}

class Son implements Runnable {
	Account acc;
	public Son(Account acc) {
		this.acc = acc;
	}

	@Override
	public void run() {
		// synchronized(acc){
		this.acc.withdrawal("1001", "123456", 1200D);
		// }
	}
}

// 银行账户
// this = 0x11223344
class Account {
	String cardNo;
	String password;// w
	double balance;

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

	// 取款
	public void withdrawal(String no, String pwd, double money) {
		// Wife
		synchronized (this) {// this = acc (0x11223344)
			System.out.println("请稍后。。。");

			if (this.cardNo.equals(no) && this.password.equals(pwd)) {

				System.out.println("验证成功,请稍后。。");

				if (money < balance) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

					balance -= money;

					System.out.println("取款成功,当前余额为:" + balance);

				} else {
					System.out.println("卡内余额不足!");
				}

			} else {
				System.out.println("卡号或密码错误!");
			}
		}
		// Husband
	}
}

4.2 线程同步(2):

同步方法:

synchronized 返回值类型 方法名称(形参列表){ //对当前对象(this)加锁 
    // 代码(原子操作) 
}
//例如:
public synchronized void push(String str) {
    
}

注:

  • 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
  • 线程退出同步方法时,会释放相应的互斥锁标记。

4.3 同步规则

  • 注意:
    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
    • 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
  • 已知JDK中线程安全的类:
    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均为synchonized修饰的同步方法。

4.4 死锁:

当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁 标记,并等待A对象锁标记时,产生死锁。

一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

在这里插入图片描述

4.5 线程通信

  • 等待://高风亮节,反受其害
    • public final void wait()
    • public final void wait(long timeout)
    • 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放其拥有的所有锁标记。同时此线程因obj处在无限期等待的状态中。释放锁,进入等待队列。
  • 通知:
    • public final void notify()
    • public final void notifyAll()
    • 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身没有任何影响。
public class TestWaitNotify {

    public static void main(String[] args) throws InterruptedException {

        Object o = new Object();

        MyThread t1 = new MyThread(o);

        MyThread t2 = new MyThread(o);

        t1.start();
        t2.start();

        Thread.sleep(10000);

        synchronized(o){

            System.out.println("main进入同步代码块");

            o.notifyAll();//从那些因为o对象而进入到无限期等待的线程中,营救一个出来

            System.out.println("main退出同步代码块");
        }


    }
}

class MyThread extends Thread{

    Object obj;

    public MyThread(Object obj){
        this.obj = obj;
    }

    public void run(){
        System.out.println(Thread.currentThread().getName() + "线程启动");


        synchronized(obj){
            System.out.println(Thread.currentThread().getName() + "进入同步代码块");
            try {
                obj.wait();//主动释放、无限期等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "退出同步代码块");
        }

        System.out.println(Thread.currentThread().getName() + "线程结束");
    }

4.6 生产者、消费者:

//容器相关,消费者不能在空的容器中拿东西,生产者不能在满的容器中放东西。

若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和 消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的 产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。

//在offer,pool加锁解决生产者、消费者问题。
//队列
public class TestProduceAndConsumer {

    public static void main(String[] args) {

        MyQueue mq = new MyQueue();

        Produce1 p1 = new Produce1(mq);
        Produce2 p2 = new Produce2(mq);
        Consumer c1 = new Consumer(mq);

        p1.start();
        p2.start();
        c1.start();


        System.out.println("main end");
    }
}

class Consumer extends Thread{
    MyQueue mq;
    public Consumer(MyQueue mq){
        this.mq = mq;
    }

    public void run(){
        for (int i = 0 ; i < 10 ; i++) {
            System.out.println(mq.poll() + "被移除");
        }
    }
}

class Produce1 extends Thread{
    MyQueue mq;
    public Produce1(MyQueue mq){
        this.mq = mq;
    }

    public void run(){
        System.out.println("Produce1启动");
        for (char ch = 'A'; ch <= 'E'; ch++) {
            mq.offer(ch);
        }
        System.out.println("Produce1结束");
    }
}

class Produce2 extends Thread{
    MyQueue mq;
    public Produce2(MyQueue mq){
        this.mq = mq;
    }

    public void run(){
        System.out.println("Produce2启动");
        for (char ch = 'F'; ch <= 'J'; ch++) {
            mq.offer(ch);
        }
        System.out.println("Produce2结束");
    }
}

//我的队列
class MyQueue {
    private List values = new ArrayList();
    private int max = 4;

    //存入队列
    public synchronized void offer(Object o){//E

        while(values.size() == max){
            //进来线程,停下
            try {
                this.wait();
                //被唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        System.out.println(Thread.currentThread().getName() + "存入第"+ (values.size() + 1) +"个元素");
        values.add(o);
    }

    //从队列取出
    public synchronized Object poll(){//1个元素

        while(values.size() == 0){
            try {
                this.wait();
                //被唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();//唤醒因mq对象而进入无限期等待的线程对象(一个)

        return values.remove(0);
    }


    public void show(){
        for (Object obj : values) {
            System.out.println(obj);
        }
    }
}
测试结果:
main end
Produce1启动
Thread-0存入第1个元素
Thread-0存入第2个元素
Thread-0存入第3个元素
Thread-0存入第4个元素
Produce2启动
Thread-1存入第4个元素
A被移除
B被移除
Thread-0存入第4个元素
Produce1结束
C被移除
Thread-1存入第4个元素
D被移除
Thread-1存入第4个元素
F被移除
Thread-1存入第4个元素
E被移除
Thread-1存入第4个元素
G被移除
Produce2结束
H被移除
I被移除
J被移除
//栈
public class TestPAC {
    public static void main(String[] args) {

        final MyStack ms = new MyStack();//临界资源

        Thread t1 = new Thread() {
            public void run() {
                for (char ch = 'A'; ch <= 'Z'; ch++) {
                    ms.push(ch+"");
                }
            }
        };

        Thread t2 = new Thread(){
            public void run(){
                for (int i = 0; i < 26; i++) {
                    ms.pop();
                }
            }
        };

        t1.start();
        t2.start();
    }
}

// Last In First Out
class MyStack {

    private String[] values = new String[] { "", "", "", "", "" };

    int size = 0;

    public synchronized void push(String str) {

        this.notifyAll();//先唤醒对方

        while (values.length == size) {
            System.out.println("满了");

            try { this.wait(); } catch (Exception e) {}
        }

        System.out.println(str + "入栈");
        values[size] = str;
        size++;
    }

    public synchronized void pop() {

        this.notifyAll();//先唤醒对方

        while (size == 0) {
            System.out.println("空了");

            try { this.wait(); } catch (Exception e) {}
        }

        System.out.println(values[size - 1] + "出栈");
        values[size - 1] = "";
        size--;
    }
}

五、总结

  • 线程的创建:

    • 方式1:继承Thread类
    • 方式2:实现Runnable接口(一个任务Task),传入给Thread对象并执行。
  • 线程安全:

    • 同步代码块:为方法中的局部代码(原子操作)加锁。
    • 同步方法:为方法中的所有代码(原子操作)加锁。
  • 线程间的通信:

    • wait() / wait(long timeout):等待
    • notify() / notifyAll():通知
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值