1 基本概念
-
程序(Program),是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
-
进程(process),是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期
-
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
-
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
-
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间 → 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全的隐患。
-
单核 CPU 和多核 CPU 的理解
-
- 单核 CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如,虽然多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么 CPU 就好比收费人员,如果有某个人不想交钱,收费人员可以把它挂起。但是CPU时间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好地发挥多线程的效率(现在的服务器都是多核的)
- 一个Java应用程序 java.exe,其实至少三个线程:main( ) 主线程,gc( ) 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
-
并行与并发
-
- 并行:多个 CPU 同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个 CPU(采用时间片)“同时”执行多个任务。比如:秒杀、多个人做同一件事。
使用多线程的好处
- 提高应用程序的响应。对图像化界面更有意义
- 提高计算机 CPU 利用率
- 改善程序结构,代码结构更清晰
何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
2 线程的创建和使用
1)多线程创建方式一:继承于Thread类
- 1.创建一个继承于
Thread
类的子类 - 2.重写 Thread 类的
run()
-->将此线程执行的操作声明在 run( ) 中 - 3.创建 Thread 类的子类的对象
- 4.通过此对象调用
start()
:作用:①启动当前线程 ②调用当前线程的 run( )
注意: 问题一:我们启动一个线程,必须调用 start( ),不能调用 run( ) 的方式启动线程。
问题二:如果再启动一个线程,必须重新创建一个 Thread 子类的对象,调用此对象的 start( )。
例子:遍历100以内继承于Thread类的子类
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread{
//2. 重写Thread类的run():将此线程要做的事声明在run方法中
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4. 通过此对象调用start():作用:① 启动当前线程 ② 调用当前线程的run()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程。
// t1.run();
//问题二:再启动一个线程,遍历100以内的偶数.不可以还让已经start()的线程去执行,会
//报Illegal ThreadStateException
// t1.start();
//我们需要重新创建一个线程的对象
MyThread t2 = new MyThread();
t2.start();
//如下操作仍然是在main线程中执行的。
for(int i = 0;i < 100;i++){
if(i % 2 == 0){
System.out.println(i + "***********");
}
}
// System.out.println("hello");
}
}
/*
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
*/
public class ThreadDemo {
public static void main(String[] args) {
MyThread1 m1 = new MyThread1();
MyThread2 m2 = new MyThread2();
m1.start();
m2.start();
//创建Thread类的匿名子类也可以
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 ==0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 !=0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2)Thread 类的有关方法
( 代码在java包中的ThreadMethodTest.java中)
-
start()
:启动当前线程;调用当前线程的run()
-
run()
:通常需要重写 Thread 类中的此方法,将创建的线程执行的操作声明在此方法中 -
currentThread()
:静态方法,返回执行当前代码的线程 -
getName()
:获取当前线程的名字 -
setName()
:设置当前线程的名字 -
yield()
:释放当前 CPU 的执行权 -
join()
:在线程 a 中调用线程b的 join( ),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完以后,线程a才结束阻塞状态。 -
stop()
:已过时。当执行此方法时,强制结束当前线程。 -
sleep(longmillitime)
:当前线程“睡眠”指定的 millitime 毫秒。在指定的 millitime 毫秒时间内,当前线程是阻塞状态的。 -
isAlive()
:判断当前线程是否存活。
3)线程的调度
4)线程的优先级
> 设置线程的优先级,在 start( ) 之前设置
> 高优先级的线程要抢占低优先级线程的 CPU 的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
5)多线程创建方式二:实现Runnable接口
- 1.创建一个实现了 Runnable 接口的类
- 2.实现类去实现 Runnable 中的抽象方法:run( )
- 3.创建实现类的对象
- 4.将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 5.通过 Thread 类的对象调用 start( )
//示例:
//创建一个实现Runnable接口的类
class MThread implements Runnable{
//2. 实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
MThread mThread = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
//5. 通过Thread类的对象调用start():① 启动线程 ② 调用当前线程的run()
t1.setName("线程一:");
t1.start();
Thread t2 = new Thread(mThread);
t2.setName("线程二:");
t2.start();
}
}
6)比较创建两种线程的两种方式
开发中优先选择实现 Runnable 接口的方式
原因:1.实现的方式没有类的单继承性的局限性。
2.实现的方式更适合来处理多个线程有共享数据的情况。
联系:public class Thread implements Runnable
相同点:两种方式都需要重写 run( ),将线程要执行的逻辑声明在 run( ) 中。
目前两种方式,要想启动线程,都是调用的 Thread 类中的 start( ) 。
3 线程的生命周期
4 线程的同步
例子:创建三个窗口卖票,总票数为100张
存在线程安全问题,待解决
1.问题:卖票过程中,出现了重票、错票—>出现了线程的安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
3.如何解决:当一个线程 a 在操作 ticket 的时候,其他线程不能参与进来,直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket。这种情况即使线程 a 出现了阻塞,也不能被改变。
4.在 Java中,我们通过同步机制,来解决线程安全问题。
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明: 1.操作共享数据的代码,即为需要被同步的代码。—>不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称,锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
补充:在实现 Runnable 接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器。
方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明同步的。
1.同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。
2.非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器:当前类本身
同步的方式,解决了线程的安全问题。—> 好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程;效率低。—> 局限性
public class BankTest {
}
class Bank {
private Bank() {
}
private static Bank instance = null;
public static synchronized Bank getInstance() {
//方式一:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
死锁问题
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run(){
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
面试题:synchronized与Lock的异同
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完响应的代码以后,自动地释放同步监视器
Lock需要手动地启动同步(
Lock()
),同时结束同步也需要手动地实现(unlock()
)优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源)→ 同步方法(在方法体之外)
练习
//设置的一个账户
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱:为了处理线程不安全问题加上synchronized来同步方法,操作同步代码时,只能有一个线程参与,其他线程等待
public synchronized void deposit(double amt){
if(amt > 0){
balance += amt;
//这里用一个sleep()将存钱动作放慢一些
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "存钱成功。余额为:" + balance);
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct){
this.acct = acct;
}
@Override
public void run() {
for(int i = 0;i < 3;i++){
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
//如下操作就实现了两个用户共用一个账户
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
//各自起个名字
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
5 线程的通信
所谓线程通信就是线程之间的交替执行
//实现Runnable接口的方式创建多线程
class Number implements Runnable{//①
private int number = 1;
@Override
public void run() { //②实现run方法
while (true){
//用synchronized创建同步代码块解决多线程安全问题
synchronized (this) {
//在此处唤醒一个线程,进行下去
this.notify();
if(number <= 100){
//为了展示更明显的多线程同步效果,故意让线程停留一会,如果有安全问题,提高出现的概率
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
//先进来的一个线程要阻塞,另一个线程才能进,先在此阻塞一下
//但是注意阻塞完要唤醒,不然就两个线程一直卡在那
try {
//使得调用如下wait()方法的线程进入阻塞状态
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number(); //③创建实现类的对象
Thread t1 = new Thread(number); //将实现类的对象传入Thread类的构造器中创建Thread类的对象
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
/*
执行过程:假设线程1先进来,此时notify没有用(因为另外一个线程没有阻塞),打印出数字1,接下来遇到wait后阻塞(会释放同步监视器,这样线程2拿到同步监视器权限才能进来),线程2进来执行notify会唤醒线程1,但此时线程1还是不能进入同步监视器,因为线程2还没有释放权限,接着线程2打印出数字2,遇到wait后阻塞(执行到wait遇到阻塞的同时也会释放同步监视器,此时线程1就可以进入监视器执行了...),
*/
涉及到的三个方法:
wait( ):一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify( ):一旦执行此方法,就会唤醒 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个,notifyAll( ):一旦执行此方法,就唤醒所有 wait 的线程。
说明:
1.wait( ),notify( ),notifyAll( ) 三个方法必须使用在同步代码块或同步方法中。
2.wait( ),notify( ),notifyAll( ) 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现IllegalMonitorStateException 异常
3.wait( ),notify( ),notifyAll( ) 三个方法是定义在 java.lang.Object 类中。
面试题:sleep( )和wait( )的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点: 1)两个方法声明的位置不同;Thread 类中声明 sleep( ),Object 类中声明 wait( )
2)调用的要求不同:sleep( ) 可以在任何需要的场景下调用(任何地方你想让线程睡一会都可以调用sleep)。wait( ) 必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中 sleep( ) 不会释放锁,wait( ) 会释放锁。
练习
/**
* 线程通信的应用:经典例题:生产者/消费者问题
*
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品。
* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
* 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,
* 店员会告诉消费者等一下,如果店中有产品了再通知消费者取走产品。
*
* 分析:
* 1. 是否多线程问题?是,生产者线程,消费者线程
* 2. 是否有共享数据?是,店员(或产品)
* 3. 如何解决线程安全问题?同步机制,有三种方法
* 4. 是否涉及到线程通信?是
*/
class Clerk{//店员
private int productCount = 0;
//生产产品
public synchronized void produceProduct(){
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + "开始生产第" + productCount + "个产品");
//生产了一个产品,就可以唤醒消费者了
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct(){
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费的第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品...");
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品...");
while (true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
6 JDK5.0新增线程创建方式
多线程创建方式三:实现Callable接口
何理解实现 Callable 接口的方式创建多线程比实现 Runnable 接口创建多线程方式强大?
1.call( ) 可以有返回值的
2.call( ) 可以抛出异常,被外面的操作捕获,获取异常的信息
3.Callable 是支持泛型的
//1. 创建一个实现Callable的实现类
class NumThread implements Callable{
//2. 实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3. 需要创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6. 获取Callable中call方法的返回值
//get()方法的返回值即为FutureTask构造器Callable实现类重写的call()的返回值。即上面的sum
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
多线程创建方式四:使用线程池
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多久时间会终止
面试题:创建多线程有几种方式?四种!
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//实际上返回的对象就是service1,且是ThreadPoolExecutor类的
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
//2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合使用于Runnable
//service.submit();//适合使用于Callable
//3. 关闭连接池
service.shutdown();
}
}