一、进程和线程
1、概念
进程,是计算机中正在运行的程序,是系统进行资源分配的基本单位。目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分。以前的单核CPU在同一时刻,只能有一个进程,在执行多个进程时,中间会进行进程切换,只是时间很短,我们看不出来,以此来达到运行多个进程,即宏观并行,微观串行。如今的CPU都是多核,可以达到真正的并行操作,实现宏观并行,微观也是并行。
线程,又称轻量级进程(Light Weight Process)。 进程中的一条执行路径,也是CPU的基本调度单位。 一个进程由一个或多个线程组成,彼此间完成不同的工作, 同时执行,称为多线程。
2、区别
1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
2. 一个程序运行后至少有一个进程。
3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
4. 进程间不能共享数据段地址,但是同进程的线程之间可以。
3、线程的组成
任何一个线程都具有基本的组成部分
1、CPU时间片: 操作系统(OS)会为每个线程分配执行时间。
2、 运行数据:
堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。
栈空间: 存储线程需使用的局部变量,每个线程都拥有独立的栈。
3、线程的逻辑代码.
4、线程的特点
1、线程抢占式执行
效率高
可防止单一线程长时间独占CPU
2、在单核CPU中,宏观上同时执行,微观上顺序执行
二、线程的创建
创建线程一共有三种方法
1、继承Thread类,并重写run()方法。
2、实现Runnable接口
3、实现Callable接口(主要与线程池一起使用)
1、 继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(i);
}
}
}
main方法
public class Test01 {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程="+i);
}
}
}
2、实现Runnable
下面用一个买票的例子来实现这种方式。
四个窗口同时卖票,每个窗口都有100张票。
public class MyThread implements Runnable{ private Integer ticket = 100; Lock l = new ReentrantLock(); @Override public void run() { while (true){ if (ticket>0){ ticket--; System.out.println(Thread.currentThread().getName()+"卖了一张还剩:"+ticket+"张"); }else { break; } } } }
测试:
public class Test01 { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread,"窗口A"); Thread t2 = new Thread(myThread,"窗口B"); Thread t3 = new Thread(myThread,"窗口C"); Thread t4 = new Thread(myThread,"窗口D"); t1.start(); t2.start(); t3.start(); t4.start(); } }
3、实现Callable接口
这种方式主要与线程池一起配合使用,基础用法和Runnable相似,只是这种方法通过Thread来启动的话,太麻烦。他与Runnable接口唯一不同的就是,该接口中的方法有返回值和异常抛出。
public class My implements Callable<Integer> {
private Integer sum=0;
@Override
public Integer call() throws Exception {
for (int i=1;i<=100;i++){
sum+=i;
}
return sum;
}
}
测试
public class Test {
public static void main(String[] args) throws Exception{
My m = new My();
//自建创建线程对象并提交Callable类型的任务是比较麻烦的,需要封装到一个 FutureTask类中,建议使用线程池来提交任务。
//FutureTask futureTask = new FutureTask(m);
//Thread t = new Thread(futureTask);
//t1.start();
//System.out.println(futureTask.get());
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Integer> future = executorService.submit(m1);
Integer integer = future.get();
System.out.println(integer);
}
}
4、获取线程ID和线程名称
1. 在Thread的子类中调用this.getId()或this.getName()
2. 使用Thread.currentThread().getId()和 Thread.currentThread().getName()
5、设置(修改)线程名称
1. 调用线程对象的setName()方法
2. 使用线程子类的构造方法赋值
三、线程中常用的方法
1、休眠
方法:sleep,是Thread类中的一个方法。
public static void sleep(long millis) 使当前线程主动休眠,millis毫秒。
public class TakeSleep {
public static void main(String[] args) {
T t = new T();
t.start();
for (int i=0;i<20;i++){
System.out.println("main==========="+i);
}
}
}
class T extends Thread{
@Override
public void run() {
for (int i=0;i<20;i++){
try {
Thread.sleep(100);//是当前的线程进入一段休眠状态
System.out.println("这是第"+i+"次");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
}
2、主动放弃
public static void yield() 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片
public class T extends Thread{
@Override
public void run() {
for (int i=0;i<10;i++){
Thread.yield();//可能导致t1和t2的交替的频率变得高了。当前的线程主动放弃时间片,回到就绪状态,再和其他的竞争下一次的时间片
System.out.println(Thread.currentThread().getName()+"第"+i+"次");
}
}
}
class TestYield {
public static void main(String[] args) {
T t1 = new T();
t1.setName("三");
T t2 = new T();
t2.setName("四");
t1.start();
t2.start();
}
}
3、加入
public final void join() 加入线程
当存在这个方法时,当前线程需要等待调用join()方法的线程执行完成之后,再执行当前线程。就像排队,中间有个插队的,你需要等待插队的那个买完之后,你才能买。一般用于主线程需要子线程的执行结果,或者前提操作时,用到join方法。
public class Test01 {
public static void main(String[] args) throws Exception{
ThreadJoin j = new ThreadJoin();
j.setName("A");
j.start();
j.join();
//把线程ThreadJoin加入当前的main线程中,直到ThreadJoin线程执行完毕后,main才会执行
for (int i=0;i<20;i++){
System.out.println("main==================="+i);
}
}
}
class ThreadJoin extends Thread{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"这是第"+i+"次");
}
}
}
4、守护线程
语法:线程对象.setDaemon(true);
设置为守护线程。 线程有两类:用户线程(前台线程)和守护线程(后台线程)如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。垃圾回收线程属于守护线程。
public class Test01 {
public static void main(String[] args) {
ThreadDaemon t = new ThreadDaemon();
t.setName("A");
t.setDaemon(true);//设置ThreadDaemon为守护线程,当前台线程都执行完毕后,守护线程也会自动结束
t.start();
for (int i=0;i<10;i++){
System.out.println("main==========="+i);
}
}
}
class ThreadDaemon extends Thread{
@Override
public void run() {
for (int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+"第"+i+"次");
}
}
}
四、线程安全、
1.什么是线程安全问题
就是 多线程环境中 , 且存在数据共享 , 一个线程访问的共享 数据被其他线程修改了, 那么就发生了线程安全问题 , 整个访问过程中 , 无一共享的数据被其他线程修改了 就是线程安全的,程序中如果使用成员变量, 且对成员变量进行数据修改 , 就存在数据共享问题, 也就是线程安全问题。2.为什么会有线程安全问题?
当多个线程同时共享一个全局变量,或者静态变量, 进行写的操作时, 可能会发生数据的冲突问题 ,也就是线程安全问题, 但是做读的操作不会引发线程安全问题。例如我们在写多窗口卖同一个数量的票时,就会出现重复卖票和卖不存在的票等问题。3.如和解决线程安全问题
- 使用同步机制, 使得在同一时间只能有一个线程修改共享数据。
- 消除共享数据, 即多个线程数据不共享或者共享的数据不被做修改 如果使用成员变量, 对成员变量不进行修改。
线程状态:
1、同步方式
同步代码块
语法:synchronized(临界资源对象){//对临界资源加锁
//线程代码
}
注意:每个对象都有一个互斥锁标记,用来分配给线程的。 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步代码块。线程退出同步代码块时,才会释放响应的互斥标记。
public class ThreadSaft {
private static String[] array = new String[5];
private static int index=0;
public static void main(String[] args) throws Exception{
Runnable hello = new Runnable() {
@Override
public void run() {
synchronized (array) {
if (array[index] == null) {
array[index] = "啦啦啦啦啦";
index++;
}
}
}
};
Runnable world = new Runnable() {
@Override
public void run() {
synchronized (array) {
if (array[index] == null) {
array[index] = "哈哈哈哈哈";
index++;
}
}
}
};
Thread t1 = new Thread(hello);
Thread t2 = new Thread(world);
t1.start();
t2.start();
t1.join();
t2.join();//避免t1,t2还没有执行就执行主线程
System.out.println(Arrays.toString(array));
}
}
2、同步方法
同步方法
语法:synchronized 返回值类型 方法名称(参数列表){
//原子操作
}
同步规则:只有调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。如果不调用包含同步代码块的方法,或者普通方法时,则不需要锁标记,可直接调用。
已知JDK中线程安全的类:
StringBuffer、Vector、Hashtable
以上类中公开的方法,均为synchronized修饰的同步方法。
3、手动锁(重入锁)
JDK5加入,与synchronized相比,显示定义,结构更加灵活
优势:提供更多使用性方法,功能更强大、性能更加优越。
语法:
Lock l = new ReentrantLock();//获取锁对象
l.lock();
try{
//代码
}finally{
l.unlock();
}
五、线程死锁
为什么会出现线程死锁?
当A线程拥有锁资源a时,这时A线程需要锁资源b, 而B线程拥有锁资源b,这时B线程需要锁资源a, 这样会导致A等待B线程释放资源b, B线程等待A线程释放锁资源a。 从而二个处于永久等待。从而操作死锁。
死锁的原因:锁与锁之间有嵌套导致。
如何解决线程死锁?
1. 尽量减少锁得嵌套。
2. 可以使用一些安全类。
3. 可以使用Lock中得枷锁,设置枷锁时间。
public class Lock {
public static Object a = new Object();
public static Object b = new Object();
}
public class Boy extends Thread{
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Lock.a){
System.out.println(Thread.currentThread().getName()+"获取到A资源");
synchronized (Lock.b){
System.out.println(Thread.currentThread().getName()+"获取到B资源");
System.out.println("完成");
}
}
}
}
public class Girl extends Thread{
@Override
public void run() {
synchronized (Lock.b){
System.out.println(Thread.currentThread().getName()+"获取到A资源");
synchronized (Lock.a){
System.out.println(Thread.currentThread().getName()+"获取到B资源");
System.out.println("完成");
}
}
}
}
测试:
public class Test {
public static void main(String[] args) {
Boy b = new Boy();
Girl g = new Girl();
b.setName("A线程");
g.setName("B线程");
b.start();
g.start();
}
}
六、线程通信
等待:释放锁,进入等待队列
public final void wait()
public final void wait(long timeout)
通知:唤醒等待队列中的线程,进入到就绪队列,参与CPU的竞争
public final void notify();唤醒一个
public final void notifyAll()唤醒所有
这些方法并不是Thread中的,而是Object类中的方法。
public class BankCard01 {
private double balance;
private boolean flag = false;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public synchronized void save(double money){
if (flag==true){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//存钱
this.balance = this.balance+money;
System.out.println(Thread.currentThread().getName()+"存入了"+money+"现在卡里的金额为:"+this.balance);
flag=true;
this.notify();
}
public synchronized void task(double money){
if (flag==false){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
this.balance = this.balance-money;
System.out.println(Thread.currentThread().getName()+"取出了"+money+"现在卡里的金额为:"+this.balance);
flag=false;
this.notify();
}
}
public class Test02 {
public static void main(String[] args) {
BankCard01 bankCard01 = new BankCard01();
BoyTask b = new BoyTask(bankCard01);
GirlTask g = new GirlTask(bankCard01);
b.setName("king");
g.setName("马闹鸡");
b.start();
g.start();
}
}
//存钱
class BoyTask extends Thread{
private BankCard01 bankCard01;
public BoyTask(BankCard01 bankCard01){
this.bankCard01 = bankCard01;
}
@Override
public void run() {
for (int i=0;i<10;i++){
bankCard01.save(1000);
}
}
}
//取钱
class GirlTask extends Thread{
private BankCard01 bankCard01;
public GirlTask(BankCard01 bankCard01){
this.bankCard01 = bankCard01;
}
@Override
public void run() {
for (int i=0;i<10;i++){
bankCard01.task(1000);
}
}
}
七、线程池
线程池类似与连接池,该池子中预先存储若干个线程对象,整个池子就是线程池。
作用:
线程容器,可设定线程分配的数量上限
将预先创建的线程对象存入池中,并重用线程池中的线程对象
避免频繁的创建和销毁
线程池的创建:
一共有四种线程池,根类为 Executor 类,ExecutorService:线程池的子接口。
常用方法:
shutdown():关闭线程池,需要等待线程池中任务执行完毕后才会关闭
shutdownNow():立即关闭线程池
isTerminated():判断线程池是否终止了
submit():提交任务给线程池中线程对象、Runnable和Callable类型的任务
public class Test { public static void main(String[] args) { //创建单一线程池:适应场景:队列要求线程有序执行 //ExecutorService executorService = Executors.newSingleThreadExecutor(); //创建固定长度的线程池对象 //ExecutorService executorService = Executors.newFixedThreadPool(3); //创建可变长度的线程池 //ExecutorService executorService = Executors.newCachedThreadPool(); //创建延迟线程池对象 ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3); for (int i=0;i<5;i++){ executorService.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"~~~~~~~~~~~~~~~~~~~~~~"); } },3,TimeUnit.SECONDS); } executorService.shutdown(); } }
在实际工作中不推荐使用 Executor 来创建线程池,而是使用原始的方法来创建线程池。
int corePoolSize, 核心线程数
int maximumPoolSize, 最大线程数
long keepAliveTime, 空闲时间
TimeUnit unit, 时间单位
BlockingQueue<Runnable> workQueue 堵塞队列public class Test { public static void main(String[] args) { BlockingQueue blockingQueue = new LinkedBlockingDeque(3); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,5,10, TimeUnit.SECONDS,blockingQueue); for (int i=0;i<5;i++){ threadPoolExecutor.submit(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"~~~~~"); } }); } threadPoolExecutor.shutdown(); } }