多线程
含义:
多线程,说白了就是多条执行路径,原来是一条路径,就主路径(main),现在是多
条路径。
常见概念:
程序:
Java源程序和字节码文件被称为“程序” ,是一个静态的概念。
进程:
执行中的程序叫做进程,是一个动态的概念。
- 进程是程序的一次动态执行过程, 占用特定的地址空间。
- 每个进程由3部分组成:
cpu
,data
,code
。每个进程都是独立的,保有自己的cpu
时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西。 - 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占
Cpu
的使用权。 - 进程查看方式:①Windows系统:
Ctrl+Alt+Del
②Unix系统:ps or top
\
线程:
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 一个进程可拥有多个并行的(concurrent)线程
- 一个进程中的线程共享相同的内存单元/内存地址空间可以访问相同的变量和对象,而且它们从同一堆中分配对象通信、数据交换、同步操作
- 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
注:
程序是指令的集合,代码的集合 ; 而进程是动态的概念,当程序在执行时,系统分配进程 ; 多线程是在同一进程下,充分利用资源 ,多条执行路径,共享资源 (cpudata code
) 。
优点:
资源利用率更好;程序设计在某些情况下更简单;程序响应更快。
缺点
设计更复杂。
创建多线程方法
方法一: 继承 Thread
类 +重写 run()
方法
public class TestThread {
public static void main(String[] args) { ----------->(主线程)main方法
// 创建线程类对象
SomeThread oneThread = new SomeThread(); ----------->new 创建对象
// 启动线程
oneThread.start(); ----------->调用start()方法启动另一个线程
}
}
// 创建线程类
class SomeThead extends Thread{
@Override
public void run(){
System.out.println("我是重写的run方法");
}
}
方法二: 实现Runnable
接口的实现类 + 重写run()
方法
public class TestThread2 implements Runnable {
public static void main(String[] args) {
//创建线程对象
SomeRunnable r1 = new SomeRunnable(); ----------->创建线程对象
Thread thread1 = new Thread(r1); ----------->目地是为了使用它的start方法
//开启线程
thread1.start(); ----------->调用start()方法
//创建线程对象
Thread thread2 = new Thread(new SomeRunnable());----------->创建线程对象
//开启线程
thread2.start(); ----------->调用start()方法
}
}
// 创建Runnable子类
class SomeRunnable implements Runnable{
@Override
public void run(){
System.out.println("我是重写的run方法");
}
}
方法三: 创建实现Callable
接口的实现类 + 重写 call()
方法 (了解)
public class CallAbleTest {
public static void main(String[] args) throws Exception{
MyCallable callable = new MyCallable();
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 将FutureTask包装成Thread
new Thread(futureTask).start();
System.out.println(futureTask.isDone());
System.out.println(futureTask.get());
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
return sum;
}
}
- Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
- .
Runnable
: 实现接口,比Thread类更加灵活,没有单继承的限制 Callable:
Thread
和Runnable
都是重写的run()方法并且没有返回值,Callable
是重写的call()方法并且有返回值并可以借助FutureTask
类来判断线程是否已经执行完毕或者取消线程执行- 当线程不需要返回值时使用
Runnable
,需要返回值时就使用Callable
,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程 hread
类是实现Runnable
,Callable
封装成FutureTask
,FutureTask
实现RunnableFuture
,RunnableFuture
继承Runnable
,所以Callable
也算是一种Runnable
,所以三种实现方式本质上都是Runnable
实现
线程池
- 线程池就是首先创建一些线程,它们的集合称为线程池。
线程池的工作机制
- 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
- 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
使用线程池的原因
- 多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了
线程的五种状态
新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
**就绪状态 **
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM
里线程调度器的调度。
**运行状态 **
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O处理完毕,线程重新转入就绪状态。
停止线程
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它的全部工作;另一个是线程被强制终止,如通过执行 stop或 destroy 方法来终止一个线程。但是,不要调用 stop,destory 方法 ,太暴力,一盆冷水让其停止。
public class TestThreadCiycle implements Runnable {
String name;
boolean live = true;
public TestThreadCiycle(String name) {
super();
this.name = name;
}
public void run() {
int i=0;
while(live){
System.out.println(name+(i++));
}
}
public void terminate(){
live = false;
}
public static void main(String[] args) {
TestThreadCiycle ttc = new TestThreadCiycle("线程A:");
Thread t1 = new Thread(ttc); //新生状态
t1.start(); //就绪状态
for(int i=0;i<1000;i++){
System.out.println(i);
}
ttc.terminate();
System.out.println("ttc stop!");
}
}
有三种方法可以让我们暂停Thread执行:
1.sleep方法:
需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。
2.yield方法:
yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行yield() 的线程有可能在进入到可执行状态后马上又被执行。让出CPU的使用权,从运行态直接进入就绪态。让CPU重新挑选哪一个线程进入运行状态。
3.join方法:
方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行
线程中常用到的方法:
线程优先级范围从 1 到 10
注意:
优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用
优先级低的线程。
线程同步和死锁问题
线程安全:
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就有可能造成数据的不准确
线程同步 synchronized
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
同
是指协同、协助、互相配合。在Java里面,通过 synchronized 进行同步的保证。
它包括两种用法:synchronized 方法
和 synchronized 块
synchronized 方法
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。
public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问
:每个对象对应一把锁,每个
synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻
塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的
线程方能获得 该锁,重新进入可执行状态。
synchronized 方法的缺陷:
若将一个大的方法声明为synchronized 将会大大影响效
率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生
命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不
会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明
为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更
好的解决办法,那就是 synchronized 块。
synchronized 块
在代码块前加上 synchronized 关键字,并指定加锁的对象
synchronized(syncObject){
//允许访问控制的代码
}
/**
* 测试同步问题
* @author Administrator
*
*/
public class TestSync {
public static void main(String[] args) {
Account a1 = new Account(100,"高");
Drawing draw1 = new Drawing(80,a1);
Drawing draw2 = new Drawing(80,a1);
draw1.start(); //你取钱
draw2.start(); //你老婆取钱
}
}
/*
* 简单表示银行账户
*/
class Account {
int money;
String aname;
public Account(int money, String aname) {
super();
this.money = money;
this.aname = aname;
}
}
/**
* 模拟提款操作
* @author Administrator
*
*/
class Drawing extends Thread{
int drawingNum; //取多少钱
Account account; //要取钱的账户
int expenseTotal; //总共取的钱数
public Drawing(int drawingNum,Account account) {
super();
this.drawingNum = drawingNum;
this.account = account;
}
@Override
public void run() {
draw();
}
void draw(){
synchronized (account) {
if(account.money-drawingNum<0){
return;
}
try {
Thread.sleep(1000); //判断完后阻塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money-=drawingNum;
expenseTotal+=drawingNum;
}
System.out.println(this.getName()+"--账户余
额:"+account.money);
System.out.println(this.getName()+"--总共取
了:"+expenseTotal);
}
}
上面这种方式叫做:互斥锁原理。利用互斥锁解决临界资源问题。
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
class Lipstick{
}
class Mirror{
}
class Makeup extends Thread {
int flag;
String girl;
static Lipstick lipstick=new Lipstick();
static Mirror mirror= new Mirror();
@Override
public void run() {
// TODO Auto-generated method stub
doMakeup();
}
void doMakeup(){
if(flag==0){
synchronized (lipstick) {
System.out.println(girl+"拿着口红!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror) {
System.out.println(girl+"拿着镜子!");
}
}
}
else{
synchronized (mirror) {
System.out.println(girl+"拿着镜子!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipstick) {
System.out.println(girl+"拿着口红!");
}
}
}
}
}
public class TestDeadLock {
public static void main(String[] args) {
Makeup m1 = new Makeup(); m1.girl="大丫"; m1.flag=0;
Makeup m2 = new Makeup(); m2.girl="小丫"; m2.flag=1;
m1.start();
m2.start();
}
}
如何解决死锁问题:
- 往往是程序逻辑的问题。需要修改程序逻辑。
- 尽量不要同时持有两个对象锁。 如修改成如下:
void doMakeup(){
if(flag==0){
synchronized (lipstick) {
System.out.println(girl+"拿着口红!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (mirror) {
System.out.println(girl+"拿着镜子!");
}
}
else{
synchronized (mirror) {
System.out.println(girl+"拿着镜子!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lipstick) {
System.out.println(girl+"拿着口红!");
}
}
}
}