进程和线程
进程process
进程就是操作系统中执行的程序。一个程序就是一个执行的进程实体。
每个运行中的进程,都有属于它独立的内存空间,各个进程互不影响
线程Thread
线程是一个进程中的执行单元,一个进程中可以有多个线程
多个线程可以访问同一个进程中的资源
每个线程都有一个独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。
多线程
如果一个进程中同时执行着多个线程,就称为多线程
多线程可以提高程序执行效率,如多个窗口卖票,可以加快卖票的效率
其实每个执行的Java程序都是多线程执行,main方法称为主线程,还有gc线程(守护线程)在同时执行
如有一个工厂,工厂中有很多车间,每个车间有很多流水线
工厂就是内存,车间就是各个进程,每个流水线就是一个进程中的线程
并行和并发
并行
各个进程同时执行,成为并行。
并发
多个线程同时执行,成为并发。
同步和异步
同步
所有的任务排队执行,称为同步执行
异步
在执行任务A的同时执行任务B,成为异步执行
Java中的线程
Java中,线程以对象的形式存在
Thread表示线程类
获取线程对象
-
获取当前正在运行的线程对象
Thread ct = Thread.currentThread();
-
创建一个线程对象
public static void main(String[] args) { //创建无参的自定义线程对象 MyThread t1 = new MyThread(); t1.setName("线程A"); //创建自定义线程对象,参数为线程名 MyThread t2 = new MyThread("线程B"); //让两个线程自动执行,必须调用start() t1.start(); t2.start(); }
构造方法
常用构造方法 | 说明 |
---|---|
Thread() | 创建一个默认的线程对象 |
Thread(String name) | 创建一个指定名称的线程对象 |
Thread(Runnable target) | 将一个Runnable对象包装为线程对象 |
Thread(Runnable target,String name) | 将一个Runnable对象包装为线程对象,同时设置线程名 |
线程的常用方法
方法 | |
---|---|
getId() | 获取线程id |
getName() | 获取线程名,默认Thread n |
getPriority() | 获取线程优先级,默认都是5 |
getState() | 获取线程状态 |
setName(String str) | 设置线程名 |
setProirity(int priority) | 设置线程优先级,范围1-10,值越大越先执行 |
isDaemon() | 判断线程是否为守护线程 |
setDaemon(boolean f) | 参数为true,表示设置线程为守护线程 |
start() | 让线程进入就绪状态 |
run() | 让线程获得执行权时的执行的方法 |
Thread.sleep(long n) | 设置当前线程休眠毫秒 |
Thread.currentThread() | 获取当前执行的线程对象 |
Thread.yield() | 线程让步 |
实现多线程
方式一:继承Thread类
- 1.创建一个类,继承Thread类
- 2.重写Thread类中的run方法
- 3.创建自定义的线程子类后,调用start()
自定义Thread类的继承类
package day8.com.hqyj.ThreadTest;
/*
* 实现多线程步骤
* 1.称为Thread的子类
* 2.重写run()方法
* 3.创建当前类对象后,调用start()方法
* */
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//让该线程输出1-100
System.out.println(getName()+":"+i);
}
}
public MyThread(String name){
super(name);
}
public MyThread(){
}
}
方式二:实现Runnable接口
由于Java是单继承,如果某个类已经使用了extends关键字,去继承另一个类,这时就不能再通过extends继承Thread实现多线程。
就需要实现Runnable接口的方式实现多线程。
- 1.自定义一个类,实现Runnable接口
- 2.重写run()方法,将多线程要执行的内容写在该方法中
- 3.创建Runnable接口的实现类对象
- 4.使用构造方法Thread(Runnable target)或Thread(Runnable target,String name)将上一步创建的Runnable实现类对象包装为Thread对象
自定义Runnable接口的实现类
package day8.com.hqyj.ThreadTest;
/*
* 实现多线程步骤
* 1.成为Runnable实现类
* 2.重写run()方法
* 3.
*
*/
public class MyThread2 implements Runnable{
@Override
public void run() {
//让线程输出1-100
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
main类
package day8.com.hqyj.ThreadTest;
public class Test3 {
public static void main(String[] args) {
//创建runnable接口的实现类
MyThread2 target = new MyThread2();
//由于启动多线程必须要通过Thread的start()方法,所以一定要创建Thread对象
Thread mt = new Thread(target,"线程A");//这里使用Thread(Runnable target)构造方法创建Thread对象
//让线程就绪
mt.start();
new Thread(new MyThread2(),"线程B").start();
}
}
方式三:使用匿名内部类
如果不想创建一个Runnable接口的实现类,就可以使用匿名内部类充当Runnable接口的实现类
package day8.com.hqyj.ThreadTest;
/*
* 实现多线程的方式三
* 使用匿名内部类
* */
public class Test4 {
public static void main(String[] args) {
//使用Thread(Runnable target,String name)构造方法创建线程对象
//此时new Runnable(){ @Override public void run() {}}就是一个匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
},"自定义线程").start();
//如果main方法当作一个线城时,需要先启动其他线程后,再执行main方法中的内容,否则按顺序进行
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
线程的生命和周期
线程的初始化到终止的整个过程,称为线程的生命周期。
新生状态
当线程对象被创建后,就进入了新生状态
就绪状态
当某个线程对象调用了start()方法后,就进入了就绪状态。
在这个状态下,线程对象不会做任何事情,只能等待cpu调度
运行状态
当某个线程对象得到CPU时间片(CPU执行这个线程的机会所给的时间),则进入运行状态,开始执行run()方法。
不会等待run()方法执行完毕,只会在指定的时间内尽可能的执行run()方法。只要调用完run()方法后,就会在进入就绪状态。
阻塞状态
如果某个线程遇到了sleep()方法或walt()方法,就会进入阻塞状态。
sleep()方法会在指定时间后,让线程重新就绪。
walt()方法只有在被调用nofity()或nofityAll()方法唤醒后才能重新就绪。
终止状态
让某个线程的run()方法中所有内容都执行完,就会进入终止状态,意味着该线程的使命已完成。
守护线程
如果将一个线程设置setDeamon(true),表示该线程为守护线程。
守护线程会随着其他非守护线程终止而终止。
多线程访问同一资源问题
可能出现的问题
如银行存款100,同一时刻在手机和ATM一起取钱取出,如果用多线程模拟,可能会出现两个线程都取出100的情况,要避免这种情况。
只能取出100,或取出200。
出现问题的原因
多线程是异步执行的,当一个线程完成时,不论后面还有没有代码块,下一个线程直接开始执行,最终线程已执行完而代码块还未执行,后来执行的线程可能已不满足执行条件,但已经执行。
如何解决
让线程同步(排队)执行即可。这样一来,某个线程执行run()方法时,就让其他线程等待run()方法的内容执行完毕。
synchronized关键字
这个线程可以修饰方法或代码块
修饰方法
写在方法的返回值之前,这时的方法就称为同步方法
public synchronized void (){
//会排队执行的代码
}
修饰代码块
写在一个独立的{}前,这段内容称为同步代码块
synchronized(要同步的对象或this){
//会排队执行的代码
}
原理
每个属性默认都有一把"锁",当某个线程运行到被synchronized修饰的方法时,该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其结束之后,才会释放这把锁。
使用synchronized修饰后的锁称为"悲观锁"
方法被synchronized修饰后,称为同步方法,就会让原本多线程变成了单线程。
多线程相关
-
实现多线程的方式
- 继承thread类
- 实现Runnable接口后,包装为Thread对象
- 匿名内部类
-
为什么说StringBuilder或ArrayList、HashMap是非线程安全的
package day8.com.hqyj.ThreadSafe; public class Test { public static void main(String[] args) throws InterruptedException { // StringBuilder sb = new StringBuilder(); StringBuffer sb = new StringBuffer(); //循环10次创建10个线程对象 for (int i = 0; i < 10; i++) { //创建线程对象 new Thread(new Runnable() { @Override public void run() { //每个线程都向StringBuilder对象中添加100次字符串 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 100; j++) { sb.append("hello"); } } }).start(); } Thread.sleep(5000); //如果正常,应该长度10*添加次数100*每次添加的字符5,长度5000 System.out.println(sb.length()); //如果用StringBuilder最终的长度可能不足 //如果用StringBuffer最终的长度准确 //如果用StringBuffer是线程安全的,适用于多线程 //如果用StringBuilder是非线程安全的,适用于单线程 } }
-
什么叫死锁?怎么产生?如何解决?
如果有两个人吃西餐,必须有刀和叉,但是此时只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,
都不释放自己拥有的,这时就会造成僵持的局面,这个局面就称为死锁,既不结束,也不继续。
模拟死锁出现的情况
定义两个线程,线程A先获取资源A后再获取资源B;线程B先获取资源B后再获取资源A。
如果对资源A和资源B使用了synchronized进行同步,就会在线程A先获取资源A的同时,
PersonA
/*
* 让该线程执行run()方法时,先获取knife对象,等待3s后再获取fork对象
* */
@Override
public void run() {
synchronized (paper){
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}
PersonB
/*
* 让该线程执行run()方法时,先获取fork对象,等待3s后再获取knife对象
* */
@Override
public void run() {
synchronized (paper){
synchronized (fork){
System.out.println(Thread.currentThread().getName()+"获取了fork,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (knife){
System.out.println(Thread.currentThread().getName()+"获取了knife,可以吃饭了");
}
}
}
}
死锁的解决方式
方式一
让两个线程获取资源的顺序保持一致。
如两个线程都先获取knife,再获取fork
@Override
public void run() {
synchronized (knife){
System.out.println(Thread.currentThread().getName()+"获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork){
System.out.println(Thread.currentThread().getName()+"获取了fork,可以吃饭了");
}
}
}
方式二
让两个线程在获取资源A和B之前,先获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个线程资源之后,将后续的内容执行完毕,在执行其他线程。
@Override
public void run() {
synchronized (paper){
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}