多线程那些事(三)-关键字

sleep

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权

wait

wait 方法是在 Object 类中定义的方法,作用是使当前线程进入等待队列,同时它会使当前线程释放所持
有的锁。直到有其它线程调用此对象的 notify 或者 notifyAll 方法进行唤
wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。

sleep 和 wait 对比

在这里插入图片描述

volatile 关键字

volatile 关键字实际上是 Java 提供的一种轻量级的同步手段,因为 volatile 只能保证多线程内存可见性,不能保证
操作的原子性。
任何被 volatile 修改的变量,都不会进行副本的拷贝,任何操作都即使写在主内存中。因此使用 volatile 修改的变
量的修改,所有线程都立刻可以看到。务必注意:volatile 保证可见性,但是不保证原子性

public class MyTest{
private volatile int a; //直接操作主内存,没有线程对工作内存和主内存同步
public void add(int count){
this.a=a+count;
}
}

使用 volatile 的限制:
1、对变量的写操作不依赖于当前值
2、该变量没有包含在具有其它变量的不变式中
volatile 的特性
1、保证可见性
2、保证有序性,JMM 可以禁止读写 volatile 变量前后语法的大部分重排序优化,可以保证变量的赋值操作顺序和
程序中的执行顺序一致
3、部分原子性,针对 volatile 变量的符合操作不具备原子性

线程的优先级:

1.在没有指定线程的优先级的时候,线程都带有普通的优先级。

2.线程的优先级可以分为1到10;10代表最高的优先级,1代表最低的优先级,普通优先级是5.

3.优先级最高的线程在运行时给予优先,但不能保证线程启动后立刻就进入运行状态。

4。与线程池中等待的线程相比,正在运行的线程拥有更高的优先级。

5.由调度程序来决定执行哪一个线程。

6.用setProperty()来设定用线程的优先级。

7.在线程的start方法调用之前,应该指定线程的优先级。

join

主要作用是进行线程的同步处理,可以使得线程之间的并行执行转换为串行执行。
线程实例的join()方法可以被用来join到线程执行的开始和其他线程执行的结束,所以直到其他线程运行结束这个线程才会执行。如果join的方法在线程实例中被调用,当前运行的线程会被堵塞,直到线程实例运行完成。
如中线程a中调用线程b的join方法,这时线程a就会进入阻塞状态,直到线程b执行完成。这样就可以使并行的线程串行化的执行。

yield

静态方法可以让当前正在运行的线程暂停,但是不会阻塞线程,仅仅是让出 CPU,当前线程进入就绪态,
给其它线程执行的机会,但是具体哪个线程运行取决于线程调度器,很有可能下次运行的还是当前线程
主要作用是让相同优先级的线程之间能适当的轮转执行,但是实际中无法保证 yield 达到让步的目的,因为让步的线程还是有可能被线程调度器再次选中
使用yield方法时要注意的几点:
1.yield是一个静态的本地方法(native)
2.调用yield后,yield告诉当前线程把运行机会交给线程池中有相同优先级的线程。
3.yield不能保证,当前线程迅速从运行状态切换到就绪状态。
4.yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。

案例:顺序打印t1,t2,t3;左手先于右手;

package process;

public class shuinxu {
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
				System.out.println("这是t1");
			Thread.yield();
		});
		Thread t2 = new Thread(() -> {
			try {
				t1.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("这是t2");
		});
		
		Thread t3 = new Thread(() -> {
			try {
				t2.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("这是t3");
		});
		Thread t4 = new Thread(() -> {
			System.out.println("这是左手");
		Thread.yield();
		});
		Thread t5 = new Thread(() -> {
			try {
				t4.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("这是右手");
		});
		t5.start();
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

多线程的调度机制

假设只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获取 CPU 的使用权才能执行指令。
多线程并发运行在宏观上看是同时运行,实际上是各个线程轮流获取 CPU 使用权才能执行指令。所谓线
程的调度就是按照特定的机制为多个线程分配 CPU 的使用权。主要有 2 种调度模型:分时调度模型和抢占式调度模型。Unix 系统采用的是时间片算法,windows 属于抢占式。
1、分时调度就是让所有线程轮流获取 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片。Java中的线程调度不是分时的,同时启动多个线程后,并不能保证各个线程轮流获取均等的时间片
2、JVM 属于采用的式基于时间片轮转法的抢占式的调度模型,使优先级高的线程获取更多的运行机会,如果可运行池种的线程优先级相同,则随机选中一个线程,处于运行状态的线程一致执行,直到不得不放弃 CPU
3、一个线程有 3 种情况下会放弃 CPU:
1)线程执行结束。2)当前线程由于某种原因阻塞。 3)JVM 使当前线程暂时放弃 CPU 转到就绪态

public class PriorityTest {
public static void main(String[] args) {
Thread left = new Thread(() -> {
for (int i = 0; i < 50; i++) {
System.out.println("前....");
Thread.yield();
}
});
Thread right = new Thread(() -> {
for (int i = 0; i < 50; i++) {
System.out.println("后....");
Thread.yield();
}
}
left.setPriority(Thread.MAX_PRIORITY);
left.start();
right.start();
System.out.println(right.getPriority());
}
}

synchronized

用途:保证一个时刻点上只有一个线程在执行,不会出现并发的情况,达到排队执行的目的
Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出
synchronized 的用法
3 种不同的用法:对象锁、类锁和同步块。注意:synchronized 不能修改构造器、变量等,
synchronized 支持重

同步实例方法

修饰实例方法,作用域当前实例加锁,进入同步方法需要获取当前实例的锁。
方法上添加 synchronized 关键字,例如 public synchronized void show()
注意:在一个类对象种所有的同步方法都是互斥。
1、只要有一个线程进入了当前类对象的同步方法,则不允许其它线程再次进入当前对象的
任何同步方法,但是允许进入非同步方法
2、同样当前线程可以进入当前类对象的其它同步方法(重入),也允许进入非同步方法。
当线程进入同步方法,则获取同步锁,离开同步方法则自动释放锁
3、这个锁就是当前类对象
线程安全的类:是通过使用同步方法的类,同步监视器是 this
1、该类对象可以被多线程安全的访问
2、每个线程调用该对象的任意方法后都将获取正确的结果
3、每个线程调用该对象的任何方法后,该对象状态依然保持合理状态
String、StringBuilder 和 StringBuffer
StringBuilder 是线程不安全的类,数据不安全,但是并发执行效率高,一般用于定义临时
变量
StringBuffer 是线程安全的类,数据安全,但是并发执行效率第,一般用于定义属性

同步静态方法

同样,这里synchronized 关键字告诉Java这个方法是同步的。

静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。

对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的那个静态同步方法被调用,一个类只能由一个线程同时执行。

同步代码块

使用 synchronized 修饰代码块时同时指定加锁对象,对给定对象加锁,进入同步代码块的
前提时获取指定对象的锁。建议在可能被并发访问的 goon 共享临界资源使用,通过这种方
法可以保证并发线程在任何一个时刻只有线程可以进入修改共享的临界资源的代码块(临界
区)
例如 synchronized(account){},其中 account 对象充当锁或者监视器,同步监视器可以阻
止多个线程同时修改同一个 account 对象。任何时刻只能有一个线程可以获取同步监视器
的锁定,当同步代码块执行完成后,该线程就睡释放该同步监视器的锁定。

代码验证和理解

package process;

public class tongbu {
public static void main(String[] args) {
		
		App1 app1=new App1();
		App1 app2=new App1();
		new Thread(){
			public void run(){
				app1.aaa();
			}
		}.start();
		Thread t1=new Thread(){
			public void run(){
				app2.bbb();
			}
		};
		t1.setPriority(Thread.MAX_PRIORITY);
		t1.start();
	}

	public static class App1 {
		public  static synchronized void aaa() {
			long k1 = System.currentTimeMillis();
			System.out.println(Thread.currentThread().getName() + "...aaa..." + k1);
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		public static synchronized void bbb() {
			long k1 = System.currentTimeMillis();
			System.out.println(Thread.currentThread().getName() + "...bbb..." + k1);
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
} 



1.两个线程分别调用同一个对象种的不同方法 aaa 和 bbb,一个同步一个不同步,会不会有
等待问题
结论:同步方法之间可以达到互斥等待的目的,同步和非同步方法之间没有任何影响
2.两个方法都有同步
结论:会出现一个等待另外一个的情况
3.一个静态一个非静态
结论:没有出现等待,这是因为静态使用的是类锁,非静态使用的是对象锁。两者并没有出
现互斥
4.两个方法都是静态
结论:出现等待,因为都使用使用的类锁。即使是创建多个对象,仍旧会出现互斥等待的效
果。这是因为不管创建多少个对象,但是类只有一个

Lock

Lock 接口是 Java1.5 引入的线程同步工具,主要用于多线程下共享资源的控制,可以通过显式定义同步锁对象来实
现同步处理,可以提供比 synchronized 更广泛灵活的锁定操作,并支持多个相关的 Condition 对象
void lock()用于尝试获取锁,如果获取到锁则返回,否则阻塞当前线程等待
void lockInterruptibly()尝试获取锁,线程可以在成功获取锁之前被中断,则放弃获取锁操作,而抛出异常
boolean tryLock()尝试获取锁,获取成功则返回 true,否则也是立即返回 false,不会阻塞
boolean tryLock(long,TimeUnit)尝试获取锁,获取到则立即返回 true,否则会阻塞一端时间等待,超时时间显示
为 long + 单位 TimeUnit,线程在成功获取锁之前可以被中断,则放弃获取锁操作,而抛出异常
void unlock()释放锁
Condition newCondition()返回当前锁的条件对象,通过条件对象可以实现类似 notify/wait 的功能,一个锁可以
有多个条件变量
Lock 接 口 在 juc 包 【 java.util.concurrent 】 中 提 供 了 3 种 实 现 , ReentrantLock 重 入 锁 、 另 外 是
ReentrantReadWriteLock 类种的两个静态内部类 ReadLock 读锁和 WriteLock 写锁
注意:ReentrantLock 是重入锁,当前线程已经调用 lock()方法持有锁时,还允许再次调用 lock 方法申请锁,但是
申请多少次,必须保证释放 unlock()方法多少次。一般建议使用 try/finally 结构释放 lock 一般编程结构
属性 private final [static] Lock lock=new ReentrantLock();
具体方法种使用锁
lock.lock(); //申请锁操作,注意会有阻塞
try{
… } finally {
lock.unlock(); //必须释放锁
}

public class 输出资源 {
private static final Lock lock = new ReentrantLock();
private final Condition aCondition = lock.newCondition();
private final Condition bCondition = lock.newCondition();
volatile boolean isA = false;
public void printA() {
lock.lock();
try {
while (isA) {
try {
aCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A" + Thread.currentThread().getName() + " ");
isA = true;
bCondition.signalAll();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while(!isA){
try {
bCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B"+Thread.currentThread().getName());
isA=false;
aCondition.signalAll();
} finally {
lock.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
输出资源 res = new 输出资源();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int k = 0; k < 20; k++)
res.printA();
}).start();
new Thread(() -> {
for (int k = 0; k < 20; k++)
res.printB();
}).start();
}
}
}在这里插入代码片

当调用 condition.await()方法阻塞线程时会自动释放锁【类似 obj.wait()】,不管当前线程调用了多少次 lock.lock()
获取锁,这时阻塞在 lock.lock()方法上其它线程就可以获取锁
当调用 condition.signall()唤醒执行继续上次阻塞的线程【类似于 obj.notify()和 notifyAll()】,从上次阻塞的位置
继续执行,默认会自动重新获取锁,注意和阻塞时获取锁的次数一致
ReentrantReadWriteLock 是 Lock 接口的另外一种实现,ReentrantLock 是一种排他锁,同一个时间只允许一个
线程访问,但是 ReentrantReadWriteLock 可以分为读锁和写锁,允许多个读线程同时访问,但是不允许写线程和
读线程、写线程和写线程同时访问。在实际应用种大部分对共享数据的访问都是读操作远多于写操作,这种情况下
使用 ReentrantReadWriteLock 能够比排他锁有更好的并发性和吞吐

Condition

条件变量就是表示条件的一种变量,这里的条件实际上没有实际含义,仅仅只是一个标记,并且条件的含义往往是
通过代码赋予含义的。条件变量都实现了 Condition 接口,条件变量的实例化是通过 lock 对象上调用
newCondition 方法来获取的。这样达到将条件和一个锁对象进行绑定的目的。可以通过 Condition 来控制并发程
序访问竞争资源的安全。
await()导致当前线程阻塞等待,直到其它线程调用该 condition 的 sign 或者 signall 方法来进行唤醒
sign()唤醒在 lock 对象上处于阻塞等待的单个线程,如果有多个等待线程则会随机选中一个线程。
signall()唤醒在 lock 对象上等待的所有线程,只有当前线程释放对该 lock 对象的锁定后,才可以执行被唤醒的线程

ReadWriteLock

ReentrantLock 在特定情况下使用是有局限性的。如果使用 ReentrantLock 本身是为了放置线程 A 在写数据时,
线程 B 在读数据,从而造成的数据不一致。如果线程 A 在读数据,线程 B 也在读数据,读数据是不会改变数据的,
此时加锁是没有必要。但是由于使用 ReentrantLock 还是添加上了排他锁,这样实际上就是降低了程序的性能。所
以引入读写锁
ReentrantReadWriteLock 是 ReadWriteLock 接口的实现类,实现了读写分离,读锁是共享的,写锁是独占的,
从而提供读写性能
ReadWriteLock lock=new ReentrantReadWriteLock();
Lock lock1=lock.readLock();
Condition c1=lock1.newCondition();
lock1.lock();
… … lock1.unlock()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,您的问题是关于Java多线程的存钱取钱问题。这是一个经典的多线程同步问题,可以通过使用synchronized关键字来实现线程安全。下面是一个简单的示例代码: ```java public class BankAccount { private int balance; public BankAccount(int balance) { this.balance = balance; } public synchronized void deposit(int amount) { balance += amount; System.out.println("Deposit successful. Balance: " + balance); } public synchronized void withdraw(int amount) { if (balance < amount) { System.out.println("Withdraw failed. Insufficient balance."); return; } balance -= amount; System.out.println("Withdraw successful. Balance: " + balance); } } public class Main { public static void main(String[] args) { BankAccount account = new BankAccount(1000); // Create two threads to simulate deposit and withdraw Thread depositThread = new Thread(() -> { for (int i = 0; i < 5; i++) { account.deposit(100); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread withdrawThread = new Thread(() -> { for (int i = 0; i < 5; i++) { account.withdraw(200); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); depositThread.start(); withdrawThread.start(); } } ``` 在这个示例代码中,我们创建了一个银行账户类BankAccount,并在其中实现了deposit和withdraw方法,并使用synchronized关键字来保证线程安全。 在main方法中,我们创建了两个线程来模拟存款和取款操作,每个线程执行5次操作。我们使用Thread.sleep方法来模拟每个操作之间的间隔,以便更好地观察多线程操作的结果。 当多个线程同时访问BankAccount对象的deposit和withdraw方法时,synchronized关键字可以确保每个方法只能被一个线程访问,从而避免了竞争条件和数据不一致的问题。 希望这个示例代码能够回答您的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值