初学Java---Java SE-线程(Thread)部分学习


第三部分Java SE-Java应用


第4单元 线程(Thread)部分学习笔记


Part1 动手做任务

一、创建线程的两种方式(面试题)

Java语言内在支持多线程机制。

为什么提供两种创建线程的方式?
  • 答:当一个类已经有了某个父类的话,就不能再继承Thread类了,只能实现Runnable接口。
1.方式一:继承Thread类,观察程序的输出

实现步骤:

  • (1).继承父类Thread
  • (2).重写run方法(线程主体)
public class MyThread extends Thread {
	public MyThread(String name) {
		super(name);
	}
	/**
	 * 线程做的事情在run方法里写,此方法来自父类Thread,子类重写父类方法
	 */
	public void run() {
		for (int i = 0; i < 10; i++) {
			//Thread.currentThread().getName()得到当前线程的名字
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
	public static void main(String[] args) {
		MyThread t1 = new MyThread("张三");
		t1.start();//启动线程的方法
		//t1.run();run方法能被调用,但是就失去了线程的意义
		MyThread t2 = new MyThread("李四");
		t2.start();
	}
}

特别注意:启动线程用start()方法

2.方式二:实现Runnable接口,观察程序输出

实现步骤:

  • (1).实现Runnable接口
  • (2).创建类对象
  • (3).用Thread类包装
public class MyThread1 implements Runnable {
	@Override //实现接口中的run方法
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
	public static void main(String[] args) {
		MyThread1 t1 = new MyThread1();
		Thread th1 = new Thread(t1,"张三");//必须有这一步
		th1.start();
		
		Thread th2 = new Thread(new MyThread1(),"李四");
		th2.start();
	}
}
3.线程调度简单分析

线程调度策略,与OS有关,执行代码的是CPU;

  • 在某一个时间点上,CPU能同时执行多个程序吗?
    • 答:不能,因此导致了执行过程不是确定的。

二、多个线程共享数据时引发的问题-非同步(非线程安全,效率较高)

重点:多个线程共享同一数据时,有可能产生共享问题。以下示例模拟多个线程共享一个银行账户。
场景:一个存折、一张银行卡对应的是同一个账户,同时取款。需要编写4个类:账户类,该类对象被共享;First和Second类,代表存折和银行卡;Test类是测试类,分析结果:

1.账户类Account:
public class Account {
	/** 账户余额 */
	private int balance;
	public Account(int balance) {
		this.balance = balance;
	}
	/**
	 * 取款方法
	 * @param count 要取款的金额
	 */
	public void withDraw(int count) {
		System.out.println(Thread.currentThread().getName() + "进入取款方法。");
		if (balance >= count) {
			System.out.println(Thread.currentThread().getName() + "可以取款。");
			balance = balance - count;
			System.out.println(Thread.currentThread().getName() + ":取了" + count + ";结余:" + balance);
		} else {
			System.out.println(Thread.currentThread().getName() + "取款时余额不足!!!");
		}
	}
}
2.First类:代表存折
public class First extends Thread {
	Account account;//银行账户
	public First(Account account) {
		this.account = account;//给账户初始化
	}
	public void run() {
		account.withDraw(1000);//调用取款方法
	}
}
3.Second类:代表银行卡
public class Second extends Thread {
	Account account;
	public Second(Account account) {
		this.account = account;
	}
	public void run() {
		account.withDraw(500);
	}
}
4.Test测试类
public class Test {
	public static void main(String[] args) {
		Account acc = new Account(1000);//建立账户对象,余额1000,该对象被两个线程共享
		First first = new First(acc);//下面两个进程共享同一账户
		Second second = new Second(acc);
		first.start();//启动线程
		second.start();
	}
}
  • 可能的输出结果(输出结果是不确定的,多执行几次,看输出结果):
//Thread-1进入取款方法。
//Thread-0进入取款方法。
//Thread-1可以取款。
//Thread-0可以取款。
//Thread-1:取了500;结余:500
//Thread-0:取了1000;结余:-500 //余额已经不够要取得了,居然还能取,多线程共享数据引发问题
  • 出现问题的原因:多线程共享同一数据引发的同步问题。线程的调度是由操作系统完成的,在存折线程执行取款过程中,有可能被银行卡线程打断,导致存折线程取款过程不连续,引发了上边的问题。
  • 解决方案:使用线程同步,让存折取款过程不被打断;在Account类取款方法加入同步(synchronized)关键字即可。

三、如何实现线程同步(效率降低,线程安全)

synchronized是Java关键字,称为同步关键字,在多线程共享数据时使用。

1.方式一:同步整个方法-在方法前加synchronized

线程同步就是保证多个线程共享数据,不出问题;即共享数据是安全的。

public class Account {
	/** 账户余额 */
	private int balance;
	public Account(int balance) {
		this.balance = balance;
	}
	/**
	 * 取款方法
	 * @param count 要取款的金额
	 */
	public synchronized void withDraw(int count) {
		System.out.println(Thread.currentThread().getName() + "进入取款方法。");
		if (balance >= count) {
			System.out.println(Thread.currentThread().getName() + "可以取款。");
			balance = balance - count;
			System.out.println(Thread.currentThread().getName() + ":取了" + count + ";结余:" + balance);
		} else {
			System.out.println(Thread.currentThread().getName() + "取款时余额不足!!!");
		}
	}	
}
  • 其他类没有变化,执行Test类:加上之后,取款过程不会被打断,便免了问题的发生。
2.方式二:同步局部代码(代码块)

Account类中的变化:

public class Account {
	/** 账户余额 */
	private int balance;
	public Account(int balance) {
		this.balance = balance;
	}
	/**
	 * 取款方法
	 * @param count 要取款的金额
	 */
	public void withDraw(int count) {
		System.out.println(Thread.currentThread().getName() + "进入取款方法。");
        //同步代码块
		synchronized (this) {
			if (balance >= count) {
				System.out.println(Thread.currentThread().getName() + "可以取款。");
				balance = balance - count;
				System.out.println(Thread.currentThread().getName() + ":取了" + count + ";结余:" + balance);
			} else {
				System.out.println(Thread.currentThread().getName() + "取款时余额不足!!!");
			}
		}
	}	
}

四、sleep方法

线程“睡觉”方法,在“睡觉”过程中,不释放对象锁。

public class StudentThread extends Thread {
	public void run() {
		for (int i = 0; i < 5; i++) {
			try {//sleep()方法 ,线程睡觉2秒钟
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		StudentThread st = new StudentThread();
		st.start();
	}
}

五、如何实现生产者-消费者问题

  • 涉及到:线程通信问题,多个线程之间要彼此通信。使用到的方法:wait()、notify()、notifyAll()和线程同步synchronized。

阐述的问题:生产者生产产品,放到一个盒子里,这个盒子只能放一件商品;生产者通知消费者来取,消费者取走产品后,通知生产者,可以继续生产了。如果消费者没有取走,生产者就等待;如果生产者没有生产出产品,消费者等待。

  • 共有4个类:服务员、厨师(生产者)、吃货(消费者)、测试类,厨师和吃货共享服务员
1.服务员类Clerk
//服务员类,其对象被生产者和消费者共享
public class Clerk {
	private int product = -1;// -1 表示目前服务员手里没有产品
	/**
	 * 这个方法由厨师(生产者)调用,此方法往服务员里放产品
	 * @param p 产品
	 */
	public synchronized void setProduct(int p) {
		if (this.product != -1) {//服务员手里有东西
			try {//服务员手里有东西,厨师需要等待
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.product = p;
		System.out.printf("厨师(生产者)炒出(%d)%n",this.product);
		notify();//通知等待区的一个吃货(消费者)可以继续了,,但只会有一个进程获得执行机会
	}
	/**
	 * 这个方法由吃货(消费者)调用,该方法从服务员手里取产品
	 * @return 产品
	 */
	public synchronized int getProduct() {
		if (this.product == -1) {//服务员手里没有东西
			try {//吃货需要等待
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		int p = this.product;
		System.out.printf("吃货(消费者)吃掉 (%d)%n",this.product);
		System.out.println("");
		this.product = -1;//取走产品
		notifyAll();//通知所有生产者线程,可以继续生产了,但只会有一个线程会获得执行机会
		return p;
	}
}
2.生产者类Produce
//生产者(厨师)线程类
public class Producer implements Runnable {
	private Clerk clerk;
	public Producer(Clerk clerk) {
		this.clerk = clerk;
	}
	public void run() {
		System.out.println("厨师(生产者)开始炒制菜品。。。");
		for (int produce = 1; produce <= 10; produce++) {
			try {//暂停随机时间
				Thread.sleep((int)Math.random() * 3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}//将产品交给服务员
			clerk.setProduct(produce);
		}
	}
}
3.消费者类Consumer
//消费者(吃货)线程类
public class Consumer implements Runnable {
	private Clerk clerk;
	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}
	public void run() {
		System.out.println("吃货(消费者)开始大吃。。。");
		for (int i = 1; i <= 10; i++) {
			try {//等待随机时间
				Thread.sleep((int)Math.random() * 3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}//从服务员处取走产品
			clerk.getProduct();
		}
	}
}
4.测试类Test
//测试类
public class ProductTest {
	public static void main(String[] args) {
		Clerk clerk = new Clerk();
		//生产者线程
		Thread pt = new Thread(new Producer(clerk));
		//消费者线程
		Thread ct = new Thread(new Consumer(clerk));
		pt.start();
		ct.start();
	}
}
输出结果:
//吃货(消费者)开始大吃。。。
//厨师(生产者)开始炒制菜品。。。
//厨师(生产者)炒出(1)
//吃货(消费者)吃掉 (1)

//厨师(生产者)炒出(2)
//吃货(消费者)吃掉 (2)

//厨师(生产者)炒出(3)
//吃货(消费者)吃掉 (3)

//厨师(生产者)炒出(4)
//吃货(消费者)吃掉 (4)

//厨师(生产者)炒出(5)
//吃货(消费者)吃掉 (5)

//厨师(生产者)炒出(6)
//吃货(消费者)吃掉 (6)

//厨师(生产者)炒出(7)
//吃货(消费者)吃掉 (7)

//厨师(生产者)炒出(8)
//吃货(消费者)吃掉 (8)

//厨师(生产者)炒出(9)
//吃货(消费者)吃掉 (9)

//厨师(生产者)炒出(10)
//吃货(消费者)吃掉 (10)

Part2 口述部分(面试题)

1.程序、进程和线程的区别是什么?

  • 程序:代码,实现某种功能。
  • 进程:执行当中的程序,会被载入内存,对应一个进程,进程独占系统资源,不同的进程之间,资源不能共享,进程由操作系系统管理。
  • 线程:线程包含在进程中,一个进程可能包含多个线程,多个线程共享系统资源,线程由操作系系统管理。多个线程可以“同时”执行。迅雷下载、网际快车FlashGet。

2.线程实现方式是什么?

  • 两种方式:代码见上边
    • (1)继承Thread类
    • (2)实现Runnable接口

3.线程的状态(生命周期)有哪些?

  • 面试点:线程有5个状态:创建、就绪、阻塞、运行、消亡;start()调完是就绪态。

在这里插入图片描述


4.什么是锁机制?如何理解锁机制?死锁?同步目的以及优缺点?

  • (1)答:即线程同步的机制:

  • (2)理解:synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的。Java实现多线程的同步操作很简单,只要在需要同步的方法或代码块中加入synchronized关键字,它能保证在同一时刻最多只有一个进程执行同一个对象的同步代码,可保证不会被干扰。如果同步了,哪个线程获得锁,哪个线程才能执行。

  • (3)锁只有1个;死锁:即锁死了。死锁解决不了,只能预防;例如:银行家算法。

  • (4)同步的目的:防止多线程共享数据出现问题。比如:取款问题。

  • (5)同步的优缺点:

    • 优点:数据不会出问题
    • 缺点:效率降低

5.sleep、wait、 yield等方法的分析

  • sleep()方法: 被调用时不释放锁,可以在任何地方调用;在睡眠期间一直保持状态,直到睡眠时间到。
  • yield()方法-让位方法:不释放锁,当一个线程调用该方法后,这个线程会停下,它会观察当前其它线程的执行优先级高低;如果别的线程优先级高于自己,他就会停下等待;否则他继续执行。
  • wait()方法: 被调用时释放锁,只能在同步方法或语句块中调动。
  • join()方法:一个进程加入到另一个进程中。
  • start()方法:启动进程。

6.线程方法和对象方法的区分

(1)线程方法:
  • 来自Thread类中的方法:start() 、run() 、sleep() 、yield()
(2)对象方法:
  • 来自Object类中的方法:wait()、notify() 、notifyAll()

7.如何结束线程?

  • (1)自然结束:run方法执行完毕。
  • (2)在run方法中发生异常。

8.什么场合使用同步?同步和异步的区别?

  • (1)答:数据被多线程共享时使用同步。
  • (2)区别:
    • 同步就是保证多个线程共享数据,不出问题;即共享数据是安全的。效率降低,线程安全。
    • 异步非线程安全,效率较高。

9.多线程共享时注意什么?

  • 答:避免彼此的 打断,导致数据的紊乱。

10.什么是并发?

  • 答:多线程同时执行,指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

11.Java线程锁有哪些?各有什么特点?

synchronized(给共享资源上锁)
  • 常用于维护数据一致性,可保证修饰的代码在执行过程中不会被其他线程干扰。使用synchronized修饰的代码具有原子性和可见性。
ReentrantLock
  • 可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
Semaphore:信号量与互斥锁
  • 通过acquire()与release()方法来获得和释放临界资源,避免死锁和读脏数据。
AtomicInteger:实现锁

  • 同步就是保证多个线程共享数据,不出问题;即共享数据是安全的。效率降低,线程安全。
    • 异步非线程安全,效率较高。

9.多线程共享时注意什么?

  • 答:避免彼此的 打断,导致数据的紊乱。

10.什么是并发?

  • 答:多线程同时执行,指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

11.Java线程锁有哪些?各有什么特点?

synchronized(给共享资源上锁)
  • 常用于维护数据一致性,可保证修饰的代码在执行过程中不会被其他线程干扰。使用synchronized修饰的代码具有原子性和可见性。
ReentrantLock
  • 可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
Semaphore:信号量与互斥锁
  • 通过acquire()与release()方法来获得和释放临界资源,避免死锁和读脏数据。
AtomicInteger:实现锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

慕兮IT

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

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

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

打赏作者

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

抵扣说明:

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

余额充值