线程的学习总结
这里写目录标题
什么是线程,什么是进程?
进程: 正在进行的程序,即内存中的应用程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
线程: 进程内部的一条执行路径或者一个控制单元。
两者的区别:
一个进程至少有一个线程
进程在执行过程中拥有独立的内存单元,而多个线程共享内存;
多线程的优势:
解决了多部分同时运行的问题,提高效率。也可能是正真的执行代码任务很少,而创建和关闭线程会导致浪费大量时间。
线程的弊端:
线程太多会导致效率的降低,因为线程的执行依靠的是CPU的来回切换。
什么叫多线程:
一个进程中有多个线程,称为多线程。
注意:多线程在只有一个cpu的情况下,不能提高程序的运行速度,但是可以提高程序的运行效率。
在一个使用cpu的情况下,如果有1000个线程执行任务,是同时做的速度快还是排队的速度快?
答:是排队的速度快,因为同时执行任务的话,线程会来回的切换去执行任务,而切换到某个线程这个线程可能只做了这个任务的百分之一甚至更低,而来回切换线程是很花费时间的。反观排队,它就是a线程做完b线程做以此类推,就会少了来回切换线程的时间代价。
实现多线程的方法
-
分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
同步与异步
同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全.
并发:两个或多个事情在同一个时间断内发生
并行:两个或多个事情在同一时刻发生
实现多线程的方法
实现多线程可以通过继承Thread类和实现Runnable接口。
- 继承Thread类
定义一个类继承Thread类
复写Thread类中的public void run()方法
将线程的任务代码封装到run方法中
在main方法种直接创建Thread的子类对象,创建线程
调用start()方法,开启线程(调用线程的任务run方法)
另外可以通过Thread的getName()获取线程的名称。
public class myThread extends Thread {
/**
* run就是线程的新的执行路径
* 触发方式不是调用run,而是通过thread对象的start()方法来启动任务
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("111" + ",第" + i + "次。");
}
}
}
public class TestMyThread {
public static void main(String[] args) {
myThread my = new myThread();
my.start();
for (int i = 0; i < 10; i++) {
System.out.println("222" + ",第" + i + "次。");
}
}
}
在这里我们要注意的是在 TestMyThread类种的main方法是主线程,在main方法中开启的线程是分支线程,他们是并发执行的,也就是他们谁快谁慢,谁先走完是不确定的,是抢占式执调度。
下面是以上线程执行的流程图:
- 实现Runnable接口;
定义一个类,实现Runnable接口;
覆盖接口的public void run()的方法,将线程的任务代码封装到run方法中;
在main方法中创建Runnable接口的子类对象
将Runnabl接口的子类对象作为参数传递给Thread类的构造函数
创建Thread类对象(原因:线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务)。
调用start()方法,启动线程。
//用于给线程进行执行的任务
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("12345" + ",第" + i + "次。");
}
}
}
public class TestRunnable {
public static void main(String[] args) {
//创建一个任务对象
MyRunnable runnable = new MyRunnable();
//2.给线程任务
Thread t = new Thread(runnable);
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("678910" + ",第" + i + "次。");
}
}
}
Runnable 接口为要实现的任务,而非线程;Runnable任务与线程是分离的,将任务给线程即可。
-
实现Runnable与继承Thread相比,有如下的优势:
* 通过创建线程任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同的任务情况。
* 可以避免单继承所带来的局限性
* 任务与线程本身是分离的,提高了程序的健壮行
* 后续学习的线程池技术,只接受Runnable类型的任务,而不接受Thread类型的线程。 -
两种方法区别:
(1)实现Runnable接口避免了单继承的局限性
(2)继承Thread类线程代码存放在Thread子类的run方法中
实现Runnable接口线程代码存放在接口的子类的run方法中;
在定义线程时,建议使用实现Runnable接口,因为几乎所有多线程都可以使用这种方式实现
start()和run方法有什么区别?
调用start方法方可启动线程,而run方法只是thread的一个普通方法,调用run方法不能实现多线程;
Start()方法:
start方法用来启动线程,实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的
代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,
一旦得到cpu时间片(执行权),就开始执行run()方法,这里方法run()称为线程体,
它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
Run()方法:
run()方法只是Thread类的一个普通方法,如果直接调用Run方法,程序中依然只有主线程这一个线程,
其程序执行路径还是只有一条,还是要等待run方法体执行完毕后才可继续执行下面的代码,
这样就没有达到多线程的目的。
Thread类的一些方法
- getName() 返回此线程的名称
- getPriority() 返回此线程的优先级
- setPriority(int newPriority) 更改此线程的优先级。
- sleep(long millis) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
- setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。
线程分为守护线程和用户线程:
用户线程:在一个进程中,不管是主线程还是子线程,直接创建的线程都是用户线程,只要有线程在做事情,程序会只有等线程全部死亡后才会结束。
守护线程:守护用户线程的,用户线程自己控制自己的死亡,而守护线程是用户线程死亡后自己自动死亡。
线程的几种状态
新建:new一个Thread对象或者其子类对象就是创建一个线程,当一个线程对象被创建,但是没有开启,这个时候,
只是对象线程对象开辟了内存空间和初始化数据。
就绪:新建的对象调用start方法,就开启了线程,线程就到了就绪状态。
在这个状态的线程对象,具有执行资格,没有执行权。
运行:当线程对象获取到了CPU的资源。
在这个状态的线程对象,既有执行资格,也有执行权。
阻塞:运行过程中的线程由于某些原因(比如wait,sleep),释放了执行资格和执行权。
当然,他们可以回到运行状态。只不过,不是直接回到。而是先回到就绪状态。
死亡:当线程对象调用的run方法结束,或者直接调用stop方法,就让线程对象死亡,在内存中变成了垃圾。
注意一点stop方法有不安全的情况,他是外部掐死这个线程,而不是线程自己自杀掉。目前stop方法被禁用,我们更多的是使用打断点标记的方式来结束线程。
1.线程排队时就是进入到阻塞状态
2.线程休眠(wait)也是无限等待,直到某个线程唤醒
3.指定时间等待可以自己醒也可以被唤醒
它们都可以和运行状态相互转换,最红走向死亡。
sleep()和wait()的区别:
(1)这两个方法来自不同的类,sleep()来自Thread类,和wait()来自Object类。
(2)sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉, 要让b线程睡觉要在b的代码中调用sleep。而wait()是Object类的非静态方法
(3)sleep()释放资源不释放锁,而wait()释放资源释放锁;
(4)使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
同步解决线程不安全问题的方式
线程不安全的原因:多个线程同时执行去争抢同一个数据,导致某个数据看到的和使用的数据不一样(有可能被其他线程插足了或是其他线程把数据改变了)
解决方法:让一个线程执行某个代码块的时候,其他线程不可以跟着同时执行这段代码,而是在方法前排队等待。
什么是锁?锁的作用是什么?
锁就是对象
锁的作用是保证线程同步,解决线程安全问题。
持有锁的线程可以在同步中执行,没有锁的线程即使获得cpu执行权,也进不去。
- 1.同步代码块:
-
可以指定需要获取哪个对象的同步锁,使用synchronized的代码块同样需要锁,但他的锁可以是任意对象,考虑到安全问题,一般还是使用同一个对象,相对来说效率较高。
注意:
**虽然同步代码快的锁可以使任何对象,但是在进行多线程通信使用同步代码快时, 必须保证同步代码快的锁的对象,否则会报错。
**同步函数的锁是this,也要保证同步函数的锁的对象和调用wait、notify和notifyAll的对象是同一个对象,也就是都是this锁代表的对象。格式: synchronized(对象) { 需同步的代码; //被同步代码块所括住的内容是会排队的。 }
-
public class Test {
public static void main(String[] args) {
//new了一个任务对象,所以票的资源只有一份;但启动的是3个线程
Runnable runnable = new Ticket();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable {
//卖的票数
private int count = 10;
//创建一个锁对象(注意,所有的线程看的是同一把锁)
private Object o = new Object();
@Override
public void run() {
while (true) {
//在这个地方我们就要同步代码块,加把锁,在代码块的里面只允许一个线程执行程序,其他的线程在这里排队等待
synchronized (o) {
if (count > 0) {
System.out.println("正在出票。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "--余票是:" + count);
}
}
}
}
}
}
- 2.同步函数(一个类中同步函数的锁是this):
-
同步方法是指进入该方法时需要获取this对象的同步锁,在方法上使用synchronized关键字,使用this对象作为锁,也就是使用了当前对象,因为锁住了方法,所以相对于代码块来说效率相对较低。
注意:静态同步函数的锁是该方法所在的类的字节码文件对象,即类名.class文件
格式:
修饰词 synchronized 返回值类型 函数名(参数列表)
{
需同步的代码;
}
-
public class Test1 {
public static void main(String[] args) {
Runnable runnable = new Ticket();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable {
//卖的票数
private int count = 10;
//创建一个锁对象(注意,所有的线程看的是同一把锁)
private Object o = new Object();
@Override
public void run() {
while (true) {
boolean flag= sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
System.out.println("正在出票。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "--余票是:" + count);
return true;
}
return false;
}
}
}
//特别注意的是
这个锁对象有点特殊:(他们的锁对象是同一个即this,可以理解为他们看着的是同一把锁),只要一个线程进入到同步代码块,锁对象被打了标记,下面的所方法同样也被打了标记(因为是同一把锁),其他的线程无论如何也进入不到下面的锁方法中。
- 3.显示锁Lock:
- 同步方法和同步代码块都是隐士锁(它内部是怎么锁的你不用管,只要加上synizable关键字他就会自动上锁自动解锁);
- 显示锁我们自己创建对象,自己上锁,自己解锁;使用如下:
public class Test3 {
public static void main(String[] args) {
Runnable runnable = new Test1.Ticket();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable {
//卖的票数
private int count = 10;
//创建一个显示锁对象 true就是公平锁,先到先得
//Lock l=new ReentrantLock(true);
Lock l=new ReentrantLock();
@Override
public void run() {
while (true) {
l.lock();
if (count > 0) {
System.out.println("正在出票。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "--余票是:" + count);
}
l.unlock();
}
}
}
}